diff --git a/source/exponential-backoff.ts b/source/exponential-backoff.ts index be17f59..57a4ee2 100644 --- a/source/exponential-backoff.ts +++ b/source/exponential-backoff.ts @@ -263,7 +263,31 @@ export class ExponentialBackoff { // Wait before going to the next attempt const delay = this.#calculateDelay(this.#options, attempt); - await new Promise((resolve) => setTimeout(resolve, delay)); + + // Wait for the delay or the abort signal + await new Promise((resolve, reject) => { + // Set a timeout to resolve the promise after the delay + let timeout: ReturnType; + + // Handle the abort signal + const abortHandler = () => { + clearTimeout(timeout); + abortController.signal.removeEventListener('abort', abortHandler); + reject(new ExponentialBackoffStoppedRetriesError(abortController.signal.reason)); + }; + + // Handle the timeout + const timeoutHandler = () => { + abortController.signal.removeEventListener('abort', abortHandler); + resolve(undefined); + }; + + // Set the timeout + timeout = setTimeout(timeoutHandler, delay); + + // Add the abort handler to the abort signal + abortController.signal.addEventListener('abort', abortHandler); + }) attempt++; } diff --git a/test/exponential-backoff.test.ts b/test/exponential-backoff.test.ts index 2eda021..aa4494b 100644 --- a/test/exponential-backoff.test.ts +++ b/test/exponential-backoff.test.ts @@ -299,6 +299,49 @@ const testExponentialBackoffRunAbortedStringCreatesError = async (): Promise => { + vi.useFakeTimers(); + + try { + let abort: (reason: unknown) => void; + + const taskFn = vi.fn(async ({ stopRetries }) => { + abort = stopRetries; + throw new Error('error message'); + }); + + // Start the exponential backoff and wait for it to complete + const result = ExponentialBackoff.run(taskFn, () => {}, { + baseDelay: 1000, + jitter: 0, + maxAttempts: 0, + }); + + // Advance the timer by 500 (mid delay) + await vi.advanceTimersByTimeAsync(500); + + // Make sure the abort function is defined (That the taskFn was called) + if (!abort!) { + throw new Error('abort is not defined'); + } + + // Check that the rsult is still pending + expect(result).not.resolves; + expect(abort).toBeDefined(); + + // Abort the exponential backoff + abort?.(new Error('exponential backoff aborted')); + + // Expect the result to be rejected with an ExponentialBackoffStoppedRetriesError + await expect(result).rejects.toThrow(ExponentialBackoffStoppedRetriesError); + } + finally { + vi.useRealTimers(); + } +}; /** * Tests the {@link ExponentialBackoff.from} factory and subsequent instance {@link ExponentialBackoff.run} * as an alternative to the static helper. @@ -617,6 +660,7 @@ const runTests = async (): Promise => { test('ExponentialBackoff: succeeds and aborts with abort signal', testExponentialBackoffRunSuccessAndAbortSignal); test('ExponentialBackoff: aborts with abort signal', testExponentialBackoffRunWithAbortSignal); test('ExponentialBackoff: aborts with aborted string creates error', testExponentialBackoffRunAbortedStringCreatesError); + test('ExponentialBackoff: aborts with abort signal, skipping delay', testExponentialBackoffRunDelayAbortedWhenAbortSignal); test('ExponentialBackoff: works via from and instance run', testExponentialBackoffFromAndInstanceRun); test('ExponentialBackoff: retries indefinitely when maxAttempts is 0', testExponentialBackoffRetriesIndefinitelyWhenMaxAttemptsIsZero); test('ExponentialBackoff: increases delay exponentially between attempts', testExponentialBackoffIncreasesDelayExponentially);