diff --git a/src/promise/timeout.spec.ts b/src/promise/timeout.spec.ts index 453a0a0dc..5ba59b19d 100644 --- a/src/promise/timeout.spec.ts +++ b/src/promise/timeout.spec.ts @@ -5,4 +5,20 @@ describe('timeout', () => { it('returns a reason if a response is received after the specified wait time', async () => { await expect(timeout(50)).rejects.toThrow('The operation was timed out'); }); + + it('should reject with AbortError when aborted via AbortSignal', async () => { + const controller = new AbortController(); + const { signal } = controller; + + setTimeout(() => controller.abort(), 50); + + await expect(timeout(1000, { signal })).rejects.toThrow('The operation was aborted'); + }); + + it('should reject immediately if signal is already aborted', async () => { + const controller = new AbortController(); + controller.abort(); + + await expect(timeout(1000, { signal: controller.signal })).rejects.toThrow('The operation was aborted'); + }); }); diff --git a/src/promise/timeout.ts b/src/promise/timeout.ts index 9b797275d..97f57e10b 100644 --- a/src/promise/timeout.ts +++ b/src/promise/timeout.ts @@ -1,10 +1,16 @@ import { delay } from './delay.ts'; import { TimeoutError } from '../error/TimeoutError.ts'; +interface TimeoutOptions { + signal?: AbortSignal; +} + /** * Returns a promise that rejects with a `TimeoutError` after a specified delay. * * @param {number} ms - The delay duration in milliseconds. + * @param {TimeoutOptions} options - The options object. + * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the timeout. * @returns {Promise} A promise that rejects with a `TimeoutError` after the specified delay. * @throws {TimeoutError} Throws a `TimeoutError` after the specified delay. * @@ -14,8 +20,18 @@ import { TimeoutError } from '../error/TimeoutError.ts'; * } catch (error) { * console.error(error); // Will log 'The operation was timed out' * } + * + * // With AbortSignal + * const controller = new AbortController(); + * const { signal } = controller; + * setTimeout(() => controller.abort(), 50); + * try { + * await timeout(1000, { signal }); // Will be aborted after 50ms + * } catch (error) { + * console.error(error); // Will log 'The operation was aborted' + * } */ -export async function timeout(ms: number): Promise { - await delay(ms); +export async function timeout(ms: number, { signal }: TimeoutOptions = {}): Promise { + await delay(ms, { signal }); throw new TimeoutError(); } diff --git a/src/promise/withTimeout.spec.ts b/src/promise/withTimeout.spec.ts index e799125db..4c4b87ecf 100644 --- a/src/promise/withTimeout.spec.ts +++ b/src/promise/withTimeout.spec.ts @@ -15,4 +15,23 @@ describe('withTimeout', () => { it('returns a reason if a response is received after the specified wait time', () => { return expect(withTimeout(() => delay(1000), 50)).rejects.toThrow('The operation was timed out'); }); + + it('should reject with AbortError when aborted via AbortSignal', async () => { + const controller = new AbortController(); + + setTimeout(() => controller.abort(), 50); + + await expect( + withTimeout(() => delay(1000), 5000, { signal: controller.signal }) + ).rejects.toThrow('The operation was aborted'); + }); + + it('should reject immediately if signal is already aborted', async () => { + const controller = new AbortController(); + controller.abort(); + + await expect( + withTimeout(() => delay(1000), 5000, { signal: controller.signal }) + ).rejects.toThrow('The operation was aborted'); + }); }); diff --git a/src/promise/withTimeout.ts b/src/promise/withTimeout.ts index 0e5dfec52..d18f45fbd 100644 --- a/src/promise/withTimeout.ts +++ b/src/promise/withTimeout.ts @@ -1,15 +1,20 @@ import { timeout } from './timeout.ts'; +interface WithTimeoutOptions { + signal?: AbortSignal; +} + /** * Executes an async function and enforces a timeout. * * If the promise does not resolve within the specified time, * the timeout will trigger and the returned promise will be rejected. * - * * @template T * @param {() => Promise} run - A function that returns a promise to be executed. * @param {number} ms - The timeout duration in milliseconds. + * @param {WithTimeoutOptions} options - The options object. + * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the operation. * @returns {Promise} A promise that resolves with the result of the `run` function or rejects if the timeout is reached. * * @example @@ -20,11 +25,19 @@ import { timeout } from './timeout.ts'; * * try { * const data = await withTimeout(fetchData, 1000); - * console.log(data); // Logs the fetched data if `fetchData` is resolved within 1 second. + * console.log(data); // Logs the fetched data if resolved within 1 second. * } catch (error) { - * console.error(error); // Will log 'TimeoutError' if `fetchData` is not resolved within 1 second. + * console.error(error); // Will log 'TimeoutError' if not resolved within 1 second. * } + * + * // With AbortSignal + * const controller = new AbortController(); + * const { signal } = controller; + * const data = await withTimeout(async () => { + * const response = await fetch('https://example.com/data', { signal }); + * return response.json(); + * }, 5000, { signal }); */ -export async function withTimeout(run: () => Promise, ms: number): Promise { - return Promise.race([run(), timeout(ms)]); +export async function withTimeout(run: () => Promise, ms: number, { signal }: WithTimeoutOptions = {}): Promise { + return Promise.race([run(), timeout(ms, { signal })]); }