Skip to content

Commit

Permalink
Support AbortController (#26)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
jopemachine and sindresorhus committed May 26, 2022
1 parent 0c28612 commit 1bf6679
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 0 deletions.
25 changes: 25 additions & 0 deletions index.d.ts
Expand Up @@ -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;
};

/**
Expand Down
42 changes: 42 additions & 0 deletions index.js
Expand Up @@ -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}\``);
Expand All @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions readme.md
Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions test.js
Expand Up @@ -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'
});
});
}

0 comments on commit 1bf6679

Please sign in to comment.