Skip to content

Commit 4875dee

Browse files
Support AbortController (#58)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 735d80e commit 4875dee

File tree

4 files changed

+117
-0
lines changed

4 files changed

+117
-0
lines changed

index.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,30 @@ export interface Options {
1818
@default true
1919
*/
2020
readonly stopOnError?: boolean;
21+
22+
/**
23+
You can abort the promises using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
24+
25+
**Requires Node.js 16 or later.*
26+
27+
@example
28+
```
29+
import pMap from 'p-map';
30+
import delay from 'delay';
31+
32+
const abortController = new AbortController();
33+
34+
setTimeout(() => {
35+
abortController.abort();
36+
}, 500);
37+
38+
const mapper = async value => value;
39+
40+
await pMap([delay(1000), delay(1000)], mapper, {signal: abortController.signal});
41+
// Throws AbortError (DOMException) after 500 ms.
42+
```
43+
*/
44+
readonly signal?: AbortSignal;
2145
}
2246

2347
/**

index.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
11
import AggregateError from 'aggregate-error';
22

3+
/**
4+
An error to be thrown when the request is aborted by AbortController.
5+
DOMException is thrown instead of this Error when DOMException is available.
6+
*/
7+
export class AbortError extends Error {
8+
constructor(message) {
9+
super();
10+
this.name = 'AbortError';
11+
this.message = message;
12+
}
13+
}
14+
15+
/**
16+
TODO: Remove AbortError and just throw DOMException when targeting Node 18.
17+
*/
18+
const getDOMException = errorMessage => globalThis.DOMException === undefined
19+
? new AbortError(errorMessage)
20+
: new DOMException(errorMessage);
21+
22+
/**
23+
TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18.
24+
*/
25+
const getAbortedReason = signal => {
26+
const reason = signal.reason === undefined
27+
? getDOMException('This operation was aborted.')
28+
: signal.reason;
29+
30+
return reason instanceof Error ? reason : getDOMException(reason);
31+
};
32+
333
export default async function pMap(
434
iterable,
535
mapper,
636
{
737
concurrency = Number.POSITIVE_INFINITY,
838
stopOnError = true,
39+
signal,
940
} = {},
1041
) {
1142
return new Promise((resolve, reject_) => {
@@ -37,6 +68,16 @@ export default async function pMap(
3768
reject_(reason);
3869
};
3970

71+
if (signal) {
72+
if (signal.aborted) {
73+
reject(getAbortedReason(signal));
74+
}
75+
76+
signal.addEventListener('abort', () => {
77+
reject(getAbortedReason(signal));
78+
});
79+
}
80+
4081
const next = async () => {
4182
if (isResolved) {
4283
return;

readme.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,30 @@ When `false`, instead of stopping when a promise rejects, it will wait for all t
7878

7979
Caveat: When `true`, any already-started async mappers will continue to run until they resolve or reject. In the case of infinite concurrency with sync iterables, *all* mappers are invoked on startup and will continue after the first rejection. [Issue #51](https://github.com/sindresorhus/p-map/issues/51) can be implemented for abort control.
8080

81+
##### signal
82+
83+
Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
84+
85+
You can abort the promises using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
86+
87+
*Requires Node.js 16 or later.*
88+
89+
```js
90+
import pMap from 'p-map';
91+
import delay from 'delay';
92+
93+
const abortController = new AbortController();
94+
95+
setTimeout(() => {
96+
abortController.abort();
97+
}, 500);
98+
99+
const mapper = async value => value;
100+
101+
await pMap([delay(1000), delay(1000)], mapper, {signal: abortController.signal});
102+
// Throws AbortError (DOMException) after 500 ms.
103+
```
104+
81105
### pMapSkip
82106

83107
Return this value from a `mapper` function to skip including the value in the returned array.

test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,31 @@ test('no unhandled rejected promises from mapper throws - concurrency 1', async
458458
test('invalid mapper', async t => {
459459
await t.throwsAsync(pMap([], 'invalid mapper', {concurrency: 2}), {instanceOf: TypeError});
460460
});
461+
462+
if (globalThis.AbortController !== undefined) {
463+
test('abort by AbortController', async t => {
464+
const abortController = new AbortController();
465+
466+
setTimeout(() => {
467+
abortController.abort();
468+
}, 100);
469+
470+
const mapper = async value => value;
471+
472+
await t.throwsAsync(pMap([delay(1000), new AsyncTestData(100), 100], mapper, {signal: abortController.signal}), {
473+
name: 'AbortError',
474+
});
475+
});
476+
477+
test('already aborted signal', async t => {
478+
const abortController = new AbortController();
479+
480+
abortController.abort();
481+
482+
const mapper = async value => value;
483+
484+
await t.throwsAsync(pMap([delay(1000), new AsyncTestData(100), 100], mapper, {signal: abortController.signal}), {
485+
name: 'AbortError',
486+
});
487+
});
488+
}

0 commit comments

Comments
 (0)