Skip to content

Commit

Permalink
Support AbortController (#59)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
cheap-glitch and sindresorhus committed Mar 19, 2022
1 parent cc75943 commit 352d62a
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 0 deletions.
27 changes: 27 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,33 @@ export interface Options extends OperationOptions {
If the `onFailedAttempt` function throws, all retries will be aborted and the original promise will reject with the thrown error.
*/
readonly onFailedAttempt?: (error: FailedAttemptError) => void | Promise<void>;

/**
You can abort retrying using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
When `AbortController.abort(reason)` is called, the promise will be rejected with `reason` as the error message.
*Requires Node.js 16 or later.*
```
import pRetry from 'p-retry';
const run = async () => { … };
const controller = new AbortController();
cancelButton.addEventListener('click', () => {
controller.abort('User clicked cancel button');
});
try {
await pRetry(run, {signal: controller.signal});
} catch (error) {
console.log(error.message);
//=> 'User clicked cancel button'
}
```
*/
readonly signal?: AbortSignal;
}

/**
Expand Down
16 changes: 16 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ const decorateErrorWithCounts = (error, attemptNumber, options) => {

const isNetworkError = errorMessage => networkErrorMsgs.has(errorMessage);

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

export default async function pRetry(input, options) {
return new Promise((resolve, reject) => {
options = {
Expand Down Expand Up @@ -76,5 +80,17 @@ export default async function pRetry(input, options) {
}
}
});

if (options.signal && !options.signal.aborted) {
options.signal.addEventListener('abort', () => {
operation.stop();
const reason = options.signal.reason === undefined
? getDOMException('The operation was aborted.')
: options.signal.reason;
reject(reason instanceof Error ? reason : getDOMException(reason));
}, {
once: true,
});
}
});
}
28 changes: 28 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,34 @@ const result = await pRetry(run, {

If the `onFailedAttempt` function throws, all retries will be aborted and the original promise will reject with the thrown error.

##### signal

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

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

When `AbortController.abort(reason)` is called, the promise will be rejected with `reason` if it's an instance of `Error`, or a `DOMException` with `reason` as its message otherwise. If no reason is provided, the promise will reject with a `DOMException`.

*Requires Node.js 16 or later.*

```js
import pRetry from 'p-retry';

const run = async () => { … };
const controller = new AbortController();

cancelButton.addEventListener('click', () => {
controller.abort('User clicked cancel button');
});

try {
await pRetry(run, {signal: controller.signal});
} catch (error) {
console.log(error.message);
//=> 'User clicked cancel button'
}
```

### AbortError(message)
### AbortError(error)

Expand Down
49 changes: 49 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,52 @@ test('throws useful error message when non-error is thrown', async t => {
message: /Non-error/,
});
});

if (globalThis.AbortController !== undefined) {
test('aborts with an AbortSignal', async t => {
t.plan(2);

let index = 0;
const controller = new AbortController();

await t.throwsAsync(pRetry(async attemptNumber => {
await delay(40);
index++;
if (attemptNumber === 3) {
controller.abort();
}

throw fixtureError;
}, {
signal: controller.signal,
}), {
instanceOf: globalThis.DOMException === undefined ? Error : DOMException,
message: 'The operation was aborted.',
});

t.is(index, 3);
});

test('preserves the abort reason', async t => {
t.plan(2);

let index = 0;
const controller = new AbortController();

await t.throwsAsync(pRetry(async attemptNumber => {
await delay(40);
index++;
if (attemptNumber === 3) {
controller.abort(fixtureError);
}

throw fixtureError;
}, {
signal: controller.signal,
}), {
is: fixtureError,
});

t.is(index, 3);
});
}

0 comments on commit 352d62a

Please sign in to comment.