diff --git a/package-lock.json b/package-lock.json index 41ad782..3351319 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7715,9 +7715,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, "funding": [ { @@ -8197,9 +8197,9 @@ } }, "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -8217,7 +8217,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/source/exponential-backoff.ts b/source/exponential-backoff.ts index be17f59..0236df9 100644 --- a/source/exponential-backoff.ts +++ b/source/exponential-backoff.ts @@ -263,7 +263,32 @@ 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 + // eslint-disable-next-line prefer-const + let timeout: ReturnType; + + // Handle the abort signal + const abortHandler = (): void => { + clearTimeout(timeout); + abortController.signal.removeEventListener('abort', abortHandler); + reject(new ExponentialBackoffStoppedRetriesError(abortController.signal.reason)); + }; + + // Handle the timeout + const timeoutHandler = (): void => { + 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..11dab60 100644 --- a/test/exponential-backoff.test.ts +++ b/test/exponential-backoff.test.ts @@ -299,6 +299,48 @@ 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 abort function is defined + 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. @@ -586,7 +628,6 @@ const testExponentialBackoffValidateOptionsRejectsNaN = (): void => { } }; - /** Tests that passing undefined into the constructor does not cause an error during spread */ const testExponentialBackoffConstructorDoesNotCauseErrorDuringSpread = async (): Promise => { const options = { @@ -617,6 +658,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);