Add StartOS 0.4.0 packaging
This commit is contained in:
+255
@@ -0,0 +1,255 @@
|
||||
export class AbortError extends Error {
|
||||
readonly name: 'AbortError';
|
||||
readonly originalError: Error;
|
||||
|
||||
/**
|
||||
Abort retrying and reject the promise. No callback functions will be called.
|
||||
|
||||
@param message - An error message or a custom error.
|
||||
*/
|
||||
constructor(message: string | Error);
|
||||
}
|
||||
|
||||
export type RetryContext = {
|
||||
readonly error: Error;
|
||||
readonly attemptNumber: number;
|
||||
readonly retriesLeft: number;
|
||||
readonly retriesConsumed: number;
|
||||
};
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
Callback invoked on each failure. Receives a context object containing the error and retry state information.
|
||||
|
||||
The function is called before `shouldConsumeRetry` and `shouldRetry`, for all errors except `AbortError`.
|
||||
|
||||
The function is not called on `AbortError`.
|
||||
|
||||
@example
|
||||
```
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async () => {
|
||||
const response = await fetch('https://sindresorhus.com/unicorn');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const result = await pRetry(run, {
|
||||
onFailedAttempt: ({error, attemptNumber, retriesLeft, retriesConsumed}) => {
|
||||
console.log(`Attempt ${attemptNumber} failed. ${retriesLeft} retries left. ${retriesConsumed} retries consumed.`);
|
||||
// 1st request => Attempt 1 failed. 5 retries left. 0 retries consumed.
|
||||
// 2nd request => Attempt 2 failed. 4 retries left. 1 retries consumed.
|
||||
// …
|
||||
},
|
||||
retries: 5
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
```
|
||||
|
||||
The `onFailedAttempt` function can return a promise. For example, to add a [delay](https://github.com/sindresorhus/delay):
|
||||
|
||||
@example
|
||||
```
|
||||
import pRetry from 'p-retry';
|
||||
import delay from 'delay';
|
||||
|
||||
const run = async () => { … };
|
||||
|
||||
const result = await pRetry(run, {
|
||||
onFailedAttempt: async () => {
|
||||
console.log('Waiting for 1 second before retrying');
|
||||
await delay(1000);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
If the `onFailedAttempt` function throws, all retries will be aborted and the original promise will reject with the thrown error.
|
||||
*/
|
||||
readonly onFailedAttempt?: (context: RetryContext) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
Decide if a retry should occur based on the context. Returning true triggers a retry, false aborts with the error.
|
||||
|
||||
The function is called after `onFailedAttempt` and `shouldConsumeRetry`.
|
||||
|
||||
The function is not called on `AbortError`, `TypeError` (except network errors), or if `retries` or `maxRetryTime` are exhausted.
|
||||
|
||||
@example
|
||||
```
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async () => { … };
|
||||
|
||||
const result = await pRetry(run, {
|
||||
shouldRetry: ({error, attemptNumber, retriesLeft}) => !(error instanceof CustomError)
|
||||
});
|
||||
```
|
||||
|
||||
In the example above, the operation will be retried unless the error is an instance of `CustomError`.
|
||||
|
||||
If the `shouldRetry` function throws, all retries will be aborted and the original promise will reject with the thrown error.
|
||||
*/
|
||||
readonly shouldRetry?: (context: RetryContext) => boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
Decide if this failure should consume a retry from the `retries` budget.
|
||||
|
||||
When `false` is returned, the failure will not consume a retry or increment backoff values, but is still subject to `maxRetryTime`.
|
||||
|
||||
The function is called after `onFailedAttempt`, but before `shouldRetry`.
|
||||
|
||||
The function is not called on `AbortError`.
|
||||
|
||||
@example
|
||||
```
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async () => { … };
|
||||
|
||||
const result = await pRetry(run, {
|
||||
retries: 2,
|
||||
shouldConsumeRetry: ({error, retriesLeft}) => {
|
||||
console.log(`Retries left: ${retriesLeft}`);
|
||||
return !(error instanceof RateLimitError);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In the example above, `RateLimitError`s will not decrement the available `retries`.
|
||||
|
||||
If the `shouldConsumeRetry` function throws, all retries will be aborted and the original promise will reject with the thrown error.
|
||||
*/
|
||||
readonly shouldConsumeRetry?: (context: RetryContext) => boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
The maximum amount of times to retry the operation.
|
||||
|
||||
@default 10
|
||||
*/
|
||||
readonly retries?: number;
|
||||
|
||||
/**
|
||||
The exponential factor to use.
|
||||
|
||||
@default 2
|
||||
*/
|
||||
readonly factor?: number;
|
||||
|
||||
/**
|
||||
The number of milliseconds before starting the first retry.
|
||||
|
||||
Set this to `0` to retry immediately with no delay.
|
||||
|
||||
@default 1000
|
||||
*/
|
||||
readonly minTimeout?: number;
|
||||
|
||||
/**
|
||||
The maximum number of milliseconds between two retries.
|
||||
|
||||
@default Infinity
|
||||
*/
|
||||
readonly maxTimeout?: number;
|
||||
|
||||
/**
|
||||
Randomizes the timeouts by multiplying with a factor between 1 and 2.
|
||||
|
||||
@default false
|
||||
*/
|
||||
readonly randomize?: boolean;
|
||||
|
||||
/**
|
||||
The maximum time (in milliseconds) that the retried operation is allowed to run.
|
||||
|
||||
@default Infinity
|
||||
|
||||
Measured with a monotonic clock (`performance.now()`) so system clock adjustments do not affect the limit.
|
||||
*/
|
||||
readonly maxRetryTime?: number;
|
||||
|
||||
/**
|
||||
You can abort retrying using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
|
||||
|
||||
```
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async () => { … };
|
||||
const controller = new AbortController();
|
||||
|
||||
cancelButton.addEventListener('click', () => {
|
||||
controller.abort(new Error('User clicked cancel button'));
|
||||
});
|
||||
|
||||
try {
|
||||
await pRetry(run, {signal: controller.signal});
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
//=> 'User clicked cancel button'
|
||||
}
|
||||
```
|
||||
*/
|
||||
readonly signal?: AbortSignal | undefined;
|
||||
|
||||
/**
|
||||
Prevents retry timeouts from keeping the process alive.
|
||||
|
||||
Only affects platforms with a `.unref()` method on timeouts, such as Node.js.
|
||||
|
||||
@default false
|
||||
*/
|
||||
readonly unref?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
Returns a `Promise` that is fulfilled when calling `input` returns a fulfilled promise. If calling `input` returns a rejected promise, `input` is called again until the max retries are reached, it then rejects with the last rejection reason.
|
||||
|
||||
Does not retry on most `TypeErrors`, with the exception of network errors. This is done on a best case basis as different browsers have different [messages](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful) to indicate this. See [whatwg/fetch#526 (comment)](https://github.com/whatwg/fetch/issues/526#issuecomment-554604080)
|
||||
|
||||
@param input - Receives the number of attempts as the first argument and is expected to return a `Promise` or any value.
|
||||
@param options - Options for configuring the retry behavior.
|
||||
|
||||
@example
|
||||
```
|
||||
import pRetry, {AbortError} from 'p-retry';
|
||||
|
||||
const run = async () => {
|
||||
const response = await fetch('https://sindresorhus.com/unicorn');
|
||||
|
||||
// Abort retrying if the resource doesn't exist
|
||||
if (response.status === 404) {
|
||||
throw new AbortError(response.statusText);
|
||||
}
|
||||
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
console.log(await pRetry(run, {retries: 5}));
|
||||
```
|
||||
*/
|
||||
export default function pRetry<T>(
|
||||
input: (attemptNumber: number) => PromiseLike<T> | T,
|
||||
options?: Options
|
||||
): Promise<T>;
|
||||
|
||||
/**
|
||||
Wrap a function so that each call is automatically retried on failure.
|
||||
|
||||
@example
|
||||
```
|
||||
import {makeRetriable} from 'p-retry';
|
||||
|
||||
const fetchWithRetry = makeRetriable(fetch, {retries: 5});
|
||||
|
||||
const response = await fetchWithRetry('https://sindresorhus.com/unicorn');
|
||||
```
|
||||
*/
|
||||
export function makeRetriable<Arguments extends readonly unknown[], Result>(
|
||||
function_: (...arguments_: Arguments) => PromiseLike<Result> | Result,
|
||||
options?: Options
|
||||
): (...arguments_: Arguments) => Promise<Result>;
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
import isNetworkError from 'is-network-error';
|
||||
|
||||
function validateRetries(retries) {
|
||||
if (typeof retries === 'number') {
|
||||
if (retries < 0) {
|
||||
throw new TypeError('Expected `retries` to be a non-negative number.');
|
||||
}
|
||||
|
||||
if (Number.isNaN(retries)) {
|
||||
throw new TypeError('Expected `retries` to be a valid number or Infinity, got NaN.');
|
||||
}
|
||||
} else if (retries !== undefined) {
|
||||
throw new TypeError('Expected `retries` to be a number or Infinity.');
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumberOption(name, value, {min = 0, allowInfinity = false} = {}) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
throw new TypeError(`Expected \`${name}\` to be a number${allowInfinity ? ' or Infinity' : ''}.`);
|
||||
}
|
||||
|
||||
if (!allowInfinity && !Number.isFinite(value)) {
|
||||
throw new TypeError(`Expected \`${name}\` to be a finite number.`);
|
||||
}
|
||||
|
||||
if (value < min) {
|
||||
throw new TypeError(`Expected \`${name}\` to be \u2265 ${min}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export class AbortError extends Error {
|
||||
constructor(message) {
|
||||
super();
|
||||
|
||||
if (message instanceof Error) {
|
||||
this.originalError = message;
|
||||
({message} = message);
|
||||
} else {
|
||||
this.originalError = new Error(message);
|
||||
this.originalError.stack = this.stack;
|
||||
}
|
||||
|
||||
this.name = 'AbortError';
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateDelay(retriesConsumed, options) {
|
||||
const attempt = Math.max(1, retriesConsumed + 1);
|
||||
const random = options.randomize ? (Math.random() + 1) : 1;
|
||||
|
||||
let timeout = Math.round(random * options.minTimeout * (options.factor ** (attempt - 1)));
|
||||
timeout = Math.min(timeout, options.maxTimeout);
|
||||
|
||||
return timeout;
|
||||
}
|
||||
|
||||
function calculateRemainingTime(start, max) {
|
||||
if (!Number.isFinite(max)) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return max - (performance.now() - start);
|
||||
}
|
||||
|
||||
async function onAttemptFailure({error, attemptNumber, retriesConsumed, startTime, options}) {
|
||||
const normalizedError = error instanceof Error
|
||||
? error
|
||||
: new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
|
||||
|
||||
if (normalizedError instanceof AbortError) {
|
||||
throw normalizedError.originalError;
|
||||
}
|
||||
|
||||
const retriesLeft = Number.isFinite(options.retries)
|
||||
? Math.max(0, options.retries - retriesConsumed)
|
||||
: options.retries;
|
||||
|
||||
const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;
|
||||
|
||||
const context = Object.freeze({
|
||||
error: normalizedError,
|
||||
attemptNumber,
|
||||
retriesLeft,
|
||||
retriesConsumed,
|
||||
});
|
||||
|
||||
await options.onFailedAttempt(context);
|
||||
|
||||
if (calculateRemainingTime(startTime, maxRetryTime) <= 0) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
const consumeRetry = await options.shouldConsumeRetry(context);
|
||||
|
||||
const remainingTime = calculateRemainingTime(startTime, maxRetryTime);
|
||||
|
||||
if (remainingTime <= 0 || retriesLeft <= 0) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
if (normalizedError instanceof TypeError && !isNetworkError(normalizedError)) {
|
||||
if (consumeRetry) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await options.shouldRetry(context)) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
if (!consumeRetry) {
|
||||
options.signal?.throwIfAborted();
|
||||
return false;
|
||||
}
|
||||
|
||||
const delayTime = calculateDelay(retriesConsumed, options);
|
||||
const finalDelay = Math.min(delayTime, remainingTime);
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
|
||||
if (finalDelay > 0) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const onAbort = () => {
|
||||
clearTimeout(timeoutToken);
|
||||
options.signal?.removeEventListener('abort', onAbort);
|
||||
reject(options.signal.reason);
|
||||
};
|
||||
|
||||
const timeoutToken = setTimeout(() => {
|
||||
options.signal?.removeEventListener('abort', onAbort);
|
||||
resolve();
|
||||
}, finalDelay);
|
||||
|
||||
if (options.unref) {
|
||||
timeoutToken.unref?.();
|
||||
}
|
||||
|
||||
options.signal?.addEventListener('abort', onAbort, {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default async function pRetry(input, options = {}) {
|
||||
options = {...options};
|
||||
|
||||
validateRetries(options.retries);
|
||||
|
||||
if (Object.hasOwn(options, 'forever')) {
|
||||
throw new Error('The `forever` option is no longer supported. For many use-cases, you can set `retries: Infinity` instead.');
|
||||
}
|
||||
|
||||
options.retries ??= 10;
|
||||
options.factor ??= 2;
|
||||
options.minTimeout ??= 1000;
|
||||
options.maxTimeout ??= Number.POSITIVE_INFINITY;
|
||||
options.maxRetryTime ??= Number.POSITIVE_INFINITY;
|
||||
options.randomize ??= false;
|
||||
options.onFailedAttempt ??= () => {};
|
||||
options.shouldRetry ??= () => true;
|
||||
options.shouldConsumeRetry ??= () => true;
|
||||
|
||||
// Validate numeric options and normalize edge cases
|
||||
validateNumberOption('factor', options.factor, {min: 0, allowInfinity: false});
|
||||
validateNumberOption('minTimeout', options.minTimeout, {min: 0, allowInfinity: false});
|
||||
validateNumberOption('maxTimeout', options.maxTimeout, {min: 0, allowInfinity: true});
|
||||
validateNumberOption('maxRetryTime', options.maxRetryTime, {min: 0, allowInfinity: true});
|
||||
|
||||
// Treat non-positive factor as 1 to avoid zero backoff or negative behavior
|
||||
if (!(options.factor > 0)) {
|
||||
options.factor = 1;
|
||||
}
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
|
||||
let attemptNumber = 0;
|
||||
let retriesConsumed = 0;
|
||||
const startTime = performance.now();
|
||||
|
||||
while (Number.isFinite(options.retries) ? retriesConsumed <= options.retries : true) {
|
||||
attemptNumber++;
|
||||
|
||||
try {
|
||||
options.signal?.throwIfAborted();
|
||||
|
||||
const result = await input(attemptNumber);
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (await onAttemptFailure({
|
||||
error,
|
||||
attemptNumber,
|
||||
retriesConsumed,
|
||||
startTime,
|
||||
options,
|
||||
})) {
|
||||
retriesConsumed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Should not reach here, but in case it does, throw an error
|
||||
throw new Error('Retry attempts exhausted without throwing an error.');
|
||||
}
|
||||
|
||||
export function makeRetriable(function_, options) {
|
||||
return function (...arguments_) {
|
||||
return pRetry(() => function_.apply(this, arguments_), options);
|
||||
};
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "p-retry",
|
||||
"version": "7.1.1",
|
||||
"description": "Retry a promise-returning or async function",
|
||||
"license": "MIT",
|
||||
"repository": "sindresorhus/p-retry",
|
||||
"funding": "https://github.com/sponsors/sindresorhus",
|
||||
"author": {
|
||||
"name": "Sindre Sorhus",
|
||||
"email": "sindresorhus@gmail.com",
|
||||
"url": "https://sindresorhus.com"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./index.js"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "xo && ava && tsd"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"promise",
|
||||
"retry",
|
||||
"retries",
|
||||
"operation",
|
||||
"failed",
|
||||
"rejected",
|
||||
"try",
|
||||
"exponential",
|
||||
"backoff",
|
||||
"attempt",
|
||||
"async",
|
||||
"await",
|
||||
"promises",
|
||||
"concurrently",
|
||||
"concurrency",
|
||||
"parallel",
|
||||
"bluebird"
|
||||
],
|
||||
"dependencies": {
|
||||
"is-network-error": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^6.3.0",
|
||||
"delay": "^6.0.0",
|
||||
"tsd": "^0.32.0",
|
||||
"xo": "^0.60.0"
|
||||
},
|
||||
"xo": {
|
||||
"rules": {
|
||||
"no-await-in-loop": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
# p-retry
|
||||
|
||||
> Retry a promise-returning or async function
|
||||
|
||||
It does exponential backoff and supports custom retry strategies for failed operations.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install p-retry
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import pRetry, {AbortError} from 'p-retry';
|
||||
|
||||
const run = async () => {
|
||||
const response = await fetch('https://sindresorhus.com/unicorn');
|
||||
|
||||
// Abort retrying if the resource doesn't exist
|
||||
if (response.status === 404) {
|
||||
throw new AbortError(response.statusText);
|
||||
}
|
||||
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
console.log(await pRetry(run, {retries: 5}));
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### pRetry(input, options?)
|
||||
|
||||
Returns a `Promise` that is fulfilled when calling `input` returns a fulfilled promise. If calling `input` returns a rejected promise, `input` is called again until the max retries are reached, it then rejects with the last rejection reason.
|
||||
|
||||
Does not retry on most `TypeErrors`, with the exception of network errors. This is done on a best case basis as different browsers have different [messages](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful) to indicate this. See [whatwg/fetch#526 (comment)](https://github.com/whatwg/fetch/issues/526#issuecomment-554604080)
|
||||
|
||||
#### input
|
||||
|
||||
Type: `Function`
|
||||
|
||||
Receives the number of attempts as the first argument and is expected to return a `Promise` or any value.
|
||||
|
||||
#### options
|
||||
|
||||
Type: `object`
|
||||
|
||||
##### onFailedAttempt(context)
|
||||
|
||||
Type: `Function`
|
||||
|
||||
Callback invoked on each failure. Receives a context object containing the error and retry state information.
|
||||
|
||||
The function is called _before_ `shouldConsumeRetry` and `shouldRetry`, for all errors _except_ `AbortError`.
|
||||
|
||||
If the function throws, all retries will be aborted and the original promise will reject with the thrown error.
|
||||
|
||||
```js
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async () => {
|
||||
const response = await fetch('https://sindresorhus.com/unicorn');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const result = await pRetry(run, {
|
||||
onFailedAttempt: ({error, attemptNumber, retriesLeft, retriesConsumed}) => {
|
||||
console.log(`Attempt ${attemptNumber} failed. ${retriesLeft} retries left. ${retriesConsumed} retries consumed.`);
|
||||
// 1st request => Attempt 1 failed. 5 retries left. 0 retries consumed.
|
||||
// 2nd request => Attempt 2 failed. 4 retries left. 1 retries consumed.
|
||||
// …
|
||||
},
|
||||
retries: 5
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
```
|
||||
|
||||
The `onFailedAttempt` function can return a promise. For example, to add a [delay](https://github.com/sindresorhus/delay):
|
||||
|
||||
```js
|
||||
import pRetry from 'p-retry';
|
||||
import delay from 'delay';
|
||||
|
||||
const run = async () => { … };
|
||||
|
||||
const result = await pRetry(run, {
|
||||
onFailedAttempt: async () => {
|
||||
console.log('Waiting for 1 second before retrying');
|
||||
await delay(1000);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
##### shouldRetry(context)
|
||||
|
||||
Type: `Function`
|
||||
|
||||
Decide if a retry should occur based on context. Returning `true` triggers a retry, `false` aborts with the error.
|
||||
|
||||
The function is called _after_ `onFailedAttempt` and `shouldConsumeRetry`.
|
||||
|
||||
The function is _not_ called on `AbortError`, `TypeError` (except network errors), or if `retries` or `maxRetryTime` are exhausted.
|
||||
|
||||
If the function throws, all retries will be aborted and the original promise will reject with the thrown error.
|
||||
|
||||
```js
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async () => { … };
|
||||
|
||||
const result = await pRetry(run, {
|
||||
shouldRetry: ({error, attemptNumber, retriesLeft}) => !(error instanceof CustomError)
|
||||
});
|
||||
```
|
||||
|
||||
In the example above, the operation will be retried unless the error is an instance of `CustomError`.
|
||||
|
||||
##### shouldConsumeRetry(context)
|
||||
|
||||
Type: `Function`
|
||||
|
||||
Decide if this failure should consume a retry from the `retries` budget.
|
||||
|
||||
When `false` is returned, the failure will not consume a retry or increment backoff values, but is still subject to `maxRetryTime`.
|
||||
|
||||
The function is called _after_ `onFailedAttempt`, but before `shouldRetry`.
|
||||
|
||||
The function is _not_ called on `AbortError`.
|
||||
|
||||
If the function throws, all retries will be aborted and the original promise will reject with the thrown error.
|
||||
|
||||
```js
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async () => { … };
|
||||
|
||||
const result = await pRetry(run, {
|
||||
retries: 2,
|
||||
shouldConsumeRetry: ({error, retriesLeft}) => !(error instanceof RateLimitError),
|
||||
});
|
||||
```
|
||||
|
||||
In the example above, `RateLimitError`s will not decrement the available `retries`.
|
||||
|
||||
##### retries
|
||||
|
||||
Type: `number`\
|
||||
Default: `10`
|
||||
|
||||
The maximum amount of times to retry the operation.
|
||||
|
||||
##### factor
|
||||
|
||||
Type: `number`\
|
||||
Default: `2`
|
||||
|
||||
The exponential factor to use.
|
||||
|
||||
##### minTimeout
|
||||
|
||||
Type: `number`\
|
||||
Default: `1000`
|
||||
|
||||
The number of milliseconds before starting the first retry.
|
||||
|
||||
Set this to `0` to retry immediately with no delay.
|
||||
|
||||
##### maxTimeout
|
||||
|
||||
Type: `number`\
|
||||
Default: `Infinity`
|
||||
|
||||
The maximum number of milliseconds between two retries.
|
||||
|
||||
##### randomize
|
||||
|
||||
Type: `boolean`\
|
||||
Default: `false`
|
||||
|
||||
Randomizes the timeouts by multiplying with a factor between 1 and 2.
|
||||
|
||||
##### maxRetryTime
|
||||
|
||||
Type: `number`\
|
||||
Default: `Infinity`
|
||||
|
||||
The maximum time (in milliseconds) that the retried operation is allowed to run.
|
||||
|
||||
Measured with a monotonic clock (`performance.now()`) so system clock adjustments do not affect the limit.
|
||||
|
||||
##### 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).
|
||||
|
||||
```js
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async () => { … };
|
||||
const controller = new AbortController();
|
||||
|
||||
cancelButton.addEventListener('click', () => {
|
||||
controller.abort(new Error('User clicked cancel button'));
|
||||
});
|
||||
|
||||
try {
|
||||
await pRetry(run, {signal: controller.signal});
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
//=> 'User clicked cancel button'
|
||||
}
|
||||
```
|
||||
|
||||
##### unref
|
||||
|
||||
Type: `boolean`\
|
||||
Default: `false`
|
||||
|
||||
Prevents retry timeouts from keeping the process alive.
|
||||
|
||||
Only affects platforms with a `.unref()` method on timeouts, such as Node.js.
|
||||
|
||||
### makeRetriable(function, options?)
|
||||
|
||||
Wrap a function so that each call is automatically retried on failure.
|
||||
|
||||
```js
|
||||
import {makeRetriable} from 'p-retry';
|
||||
|
||||
const fetchWithRetry = makeRetriable(fetch, {retries: 5});
|
||||
|
||||
const response = await fetchWithRetry('https://sindresorhus.com/unicorn');
|
||||
```
|
||||
|
||||
### AbortError(message)
|
||||
### AbortError(error)
|
||||
|
||||
Abort retrying and reject the promise. No callbacks functions will be called.
|
||||
|
||||
### message
|
||||
|
||||
Type: `string`
|
||||
|
||||
An error message.
|
||||
|
||||
### error
|
||||
|
||||
Type: `Error`
|
||||
|
||||
A custom error.
|
||||
|
||||
## Tip
|
||||
|
||||
You can pass arguments to the function being retried by wrapping it in an inline arrow function:
|
||||
|
||||
```js
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const run = async emoji => {
|
||||
// …
|
||||
};
|
||||
|
||||
// Without arguments
|
||||
await pRetry(run, {retries: 5});
|
||||
|
||||
// With arguments
|
||||
await pRetry(() => run('🦄'), {retries: 5});
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### How do I mock timers when testing with this package?
|
||||
|
||||
The package uses `setTimeout` and `clearTimeout` from the global scope, so you can use the [Node.js test timer mocking](https://nodejs.org/api/test.html#class-mocktimers) or a package like [`sinon`](https://github.com/sinonjs/sinon).
|
||||
|
||||
### How do I stop retries when the process receives SIGINT (Ctrl+C)?
|
||||
|
||||
Use an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to signal cancellation on SIGINT, and pass its `signal` to `pRetry`:
|
||||
|
||||
```js
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
controller.abort(new Error('SIGINT received'));
|
||||
});
|
||||
|
||||
try {
|
||||
await pRetry(run, {signal: controller.signal});
|
||||
} catch (error) {
|
||||
console.log('Retry stopped due to:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
The package does not handle process signals itself to avoid global side effects.
|
||||
|
||||
## Related
|
||||
|
||||
- [p-timeout](https://github.com/sindresorhus/p-timeout) - Timeout a promise after a specified amount of time
|
||||
- [More…](https://github.com/sindresorhus/promise-fun)
|
||||
Reference in New Issue
Block a user