diff --git a/index.d.ts b/index.d.ts index f8a5420..a6e8a93 100644 --- a/index.d.ts +++ b/index.d.ts @@ -41,6 +41,31 @@ export type Options = { setTimeout: typeof global.setTimeout; clearTimeout: typeof global.clearTimeout; }; + + /** + You can abort the promise using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). + + _Requires Node.js 16 or later._ + + @example + ``` + import pTimeout from 'p-timeout'; + import delay from 'delay'; + + const delayedPromise = delay(3000); + + const abortController = new AbortController(); + + setTimeout(() => { + abortController.abort(); + }, 100); + + await pTimeout(delayedPromise, 2000, undefined, { + signal: abortController.signal + }); + ``` + */ + signal?: globalThis.AbortSignal; }; /** diff --git a/index.js b/index.js index 629b868..2c50263 100644 --- a/index.js +++ b/index.js @@ -5,8 +5,39 @@ export class TimeoutError extends Error { } } +/** +An error to be thrown when the request is aborted by AbortController. +DOMException is thrown instead of this Error when DOMException is available. +*/ +export class AbortError extends Error { + constructor(message) { + super(); + this.name = 'AbortError'; + this.message = message; + } +} + +/** +TODO: Remove AbortError and just throw DOMException when targeting Node 18. +*/ +const getDOMException = errorMessage => globalThis.DOMException === undefined ? + new AbortError(errorMessage) : + new DOMException(errorMessage); + +/** +TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18. +*/ +const getAbortedReason = signal => { + const reason = signal.reason === undefined ? + getDOMException('This operation was aborted.') : + signal.reason; + + return reason instanceof Error ? reason : getDOMException(reason); +}; + export default function pTimeout(promise, milliseconds, fallback, options) { let timer; + const cancelablePromise = new Promise((resolve, reject) => { if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) { throw new TypeError(`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``); @@ -22,6 +53,17 @@ export default function pTimeout(promise, milliseconds, fallback, options) { ...options }; + if (options.signal) { + const {signal} = options; + if (signal.aborted) { + reject(getAbortedReason(signal)); + } + + signal.addEventListener('abort', () => { + reject(getAbortedReason(signal)); + }); + } + timer = options.customTimers.setTimeout.call(undefined, () => { if (typeof fallback === 'function') { try { diff --git a/readme.md b/readme.md index f091378..a6ddae5 100644 --- a/readme.md +++ b/readme.md @@ -103,6 +103,31 @@ await pTimeout(doSomething(), 2000, undefined, { }); ``` +#### signal + +Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) + +You can abort the promise using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). + +*Requires Node.js 16 or later.* + +```js +import pTimeout from 'p-timeout'; +import delay from 'delay'; + +const delayedPromise = delay(3000); + +const abortController = new AbortController(); + +setTimeout(() => { + abortController.abort(); +}, 100); + +await pTimeout(delayedPromise, 2000, undefined, { + signal: abortController.signal +}); +``` + ### TimeoutError Exposed for instance checking and sub-classing. diff --git a/test.js b/test.js index c4d172b..c9f31fb 100644 --- a/test.js +++ b/test.js @@ -88,3 +88,34 @@ test('`.clear()` method', async t => { await promise; t.true(inRange(end(), {start: 0, end: 350})); }); + +/** +TODO: Remove if statement when targeting Node.js 16. +*/ +if (globalThis.AbortController !== undefined) { + test('rejects when calling `AbortController#abort()`', async t => { + const abortController = new AbortController(); + + const promise = pTimeout(delay(3000), 2000, undefined, { + signal: abortController.signal + }); + + abortController.abort(); + + await t.throwsAsync(promise, { + name: 'AbortError' + }); + }); + + test('already aborted signal', async t => { + const abortController = new AbortController(); + + abortController.abort(); + + await t.throwsAsync(pTimeout(delay(3000), 2000, undefined, { + signal: abortController.signal + }), { + name: 'AbortError' + }); + }); +}