Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support AbortController #58

Merged
merged 8 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ export interface Options {
@default true
*/
readonly stopOnError?: boolean;

/**
You can abort the promises using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).

**Requires Node.js 16 or later.*

@example
```
import pMap from 'p-map';
import delay from 'delay';

const abortController = new AbortController();

setTimeout(() => {
abortController.abort();
}, 500);

const mapper = async value => value;

await pMap([delay(1000), delay(1000)], mapper, {signal: abortController.signal});
//=> Throws AbortError (DOMException) after 500ms.
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
```
*/
readonly signal?: AbortSignal;
}

/**
Expand Down
35 changes: 35 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import AggregateError from 'aggregate-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;
}
}

const getDOMException = errorMessage => globalThis.DOMException === undefined
? new AbortError(errorMessage)
: new DOMException(errorMessage);

jopemachine marked this conversation as resolved.
Show resolved Hide resolved
const getAbortedReason = signal => {
const reason = signal.reason === undefined
? getDOMException('This operation was aborted.')
: signal.reason;

return reason instanceof Error ? reason : getDOMException(reason);
};

export default async function pMap(
iterable,
mapper,
{
concurrency = Number.POSITIVE_INFINITY,
stopOnError = true,
signal = undefined,
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
} = {},
) {
return new Promise((resolve, reject_) => {
Expand Down Expand Up @@ -37,6 +62,16 @@ export default async function pMap(
reject_(reason);
};

if (signal) {
if (signal.aborted) {
reject(getAbortedReason(signal));
}

signal.addEventListener('abort', () => {
reject(getAbortedReason(signal));
});
}

const next = async () => {
if (isResolved) {
return;
Expand Down
24 changes: 24 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,30 @@ When `false`, instead of stopping when a promise rejects, it will wait for all t

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.

##### signal

Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)

You can abort the promises using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).

*Requires Node.js 16 or later.*

```js
import pMap from 'p-map';
import delay from 'delay';

const abortController = new AbortController();

setTimeout(() => {
abortController.abort();
}, 500);

const mapper = async value => value;

await pMap([delay(1000), delay(1000)], mapper, {signal: abortController.signal});
//=> Throws AbortError (DOMException) after 500ms.
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
```

### pMapSkip

Return this value from a `mapper` function to skip including the value in the returned array.
Expand Down
29 changes: 29 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,3 +458,32 @@ test('no unhandled rejected promises from mapper throws - concurrency 1', async
test('invalid mapper', async t => {
await t.throwsAsync(pMap([], 'invalid mapper', {concurrency: 2}), {instanceOf: TypeError});
});

if (globalThis.AbortController !== undefined) {
test('abort by AbortController', async t => {
const abortController = new AbortController();

setTimeout(() => {
abortController.abort();
}, 100);

const mapper = async value => value;

await t.throwsAsync(pMap([delay(1000), new AsyncTestData(100), 100], mapper, {signal: abortController.signal}), {
name: 'AbortError',
});
});

test('already aborted signal', async t => {
const abortController = new AbortController();

abortController.abort();

const mapper = async value => value;

await t.throwsAsync(pMap([delay(1000), new AsyncTestData(100), 100], mapper, {signal: abortController.signal}), {
name: 'AbortError',
});
});
}

jopemachine marked this conversation as resolved.
Show resolved Hide resolved