From af23dabd6cbe9e275e94477a272f259c2f7aa5b2 Mon Sep 17 00:00:00 2001 From: SeungWon Date: Fri, 29 Sep 2023 02:14:44 +0900 Subject: [PATCH 01/10] feat: Add shouldRetry option for conditional retry logic --- index.d.ts | 14 ++++++++++++++ index.js | 12 +++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 0fbcd54..79af79e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -41,6 +41,20 @@ export type Options = { */ readonly onFailedAttempt?: (error: FailedAttemptError) => void | Promise; + /** + `shouldRetry` is an optional callback function that determines whether a failed attempt should be retried or not. It receives the error thrown by `input` as the first argument. + + Returning true from `shouldRetry` will make `p-retry` attempt to retry the operation, while returning false will abort the retry process and the promise will be rejected with the provided error. + + ``` + const result = await pRetry(run, { + shouldRetry: error => !(error instanceof CustomError) + }); + ``` + In the example above, the operation will be retried unless the error is an instance of `CustomError`. + */ + readonly shouldRetry?: (error: FailedAttemptError) => boolean | Promise; + /** You can abort retrying using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). diff --git a/index.js b/index.js index a1e650c..ed2bd53 100644 --- a/index.js +++ b/index.js @@ -27,11 +27,14 @@ const decorateErrorWithCounts = (error, attemptNumber, options) => { return error; }; +const shouldRetry = error => !(error instanceof TypeError) || isNetworkError(error); + export default async function pRetry(input, options) { return new Promise((resolve, reject) => { options = { onFailedAttempt() {}, retries: 10, + shouldRetry, ...options, }; @@ -70,7 +73,14 @@ export default async function pRetry(input, options) { throw error; } - await options.onFailedAttempt(decorateErrorWithCounts(error, attemptNumber, options)); + decorateErrorWithCounts(error, attemptNumber, options); + + if (!(await options.shouldRetry(error))) { + operation.stop(); + reject(error); + } + + await options.onFailedAttempt(error); if (!operation.retry(error)) { throw operation.mainError(); From 72ab2c7c5215f3eab50e2f83f17504d42b0083ec Mon Sep 17 00:00:00 2001 From: SeungWon Date: Fri, 29 Sep 2023 02:54:32 +0900 Subject: [PATCH 02/10] test: Add test for shouldRetry option functionality --- test.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test.js b/test.js index 3184165..118e8f8 100644 --- a/test.js +++ b/test.js @@ -277,3 +277,33 @@ test('preserves the abort reason', async t => { t.is(index, 3); }); + +test('should retry only when shouldRetry returns true', async t => { + t.plan(2); + + const shouldRetryError = new Error('should-retry'); + const customError = new Error('custom-error'); + + let index = 0; + + try { + await pRetry( + async () => { + await delay(40); + index++; + const error = index < 3 ? shouldRetryError : customError; + throw error; + }, + { + async shouldRetry(error) { + return error.message === shouldRetryError.message; + }, + retries: 10, + }, + ); + } catch (error) { + t.is(error.message, customError.message); + } + + t.is(index, 3); +}); From e09451e0bd46127f5583fc6647ec1fed71d3ec90 Mon Sep 17 00:00:00 2001 From: SeungWon Date: Thu, 26 Oct 2023 01:13:22 +0900 Subject: [PATCH 03/10] refactor: reflect review --- index.d.ts | 14 +++++++++++--- index.js | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 79af79e..1b63944 100644 --- a/index.d.ts +++ b/index.d.ts @@ -42,15 +42,23 @@ export type Options = { readonly onFailedAttempt?: (error: FailedAttemptError) => void | Promise; /** - `shouldRetry` is an optional callback function that determines whether a failed attempt should be retried or not. It receives the error thrown by `input` as the first argument. + `shouldRetry` is an optional function deciding if a retry should occur based on the error. Returning true triggers a retry, false aborts with the error. - Returning true from `shouldRetry` will make `p-retry` attempt to retry the operation, while returning false will abort the retry process and the promise will be rejected with the provided error. + The `shouldRetry` function can return a promise. + @param error - The error thrown by `input`. + + @example ``` + import pRetry from 'p-retry'; + + const run = async () => { ... }; + const result = await pRetry(run, { - shouldRetry: error => !(error instanceof CustomError) + shouldRetry: error => !(error instanceof CustomError); }); ``` + In the example above, the operation will be retried unless the error is an instance of `CustomError`. */ readonly shouldRetry?: (error: FailedAttemptError) => boolean | Promise; diff --git a/index.js b/index.js index ed2bd53..2fa5821 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,7 @@ const decorateErrorWithCounts = (error, attemptNumber, options) => { return error; }; -const shouldRetry = error => !(error instanceof TypeError) || isNetworkError(error); +const shouldRetry = () => true; export default async function pRetry(input, options) { return new Promise((resolve, reject) => { From 3fda510962fb4ccbd1246675d80501076ff55fa9 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 3 Nov 2023 22:16:41 +0700 Subject: [PATCH 04/10] Update index.d.ts --- index.d.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1b63944..9da5b8e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -42,17 +42,15 @@ export type Options = { readonly onFailedAttempt?: (error: FailedAttemptError) => void | Promise; /** - `shouldRetry` is an optional function deciding if a retry should occur based on the error. Returning true triggers a retry, false aborts with the error. + Decide if a retry should occur based on the error. Returning true triggers a retry, false aborts with the error. - The `shouldRetry` function can return a promise. - - @param error - The error thrown by `input`. + @param error - The error thrown by the input function. @example ``` import pRetry from 'p-retry'; - const run = async () => { ... }; + const run = async () => { … }; const result = await pRetry(run, { shouldRetry: error => !(error instanceof CustomError); From fc43684ad262d8d334e2c9ea8891bd48c9076461 Mon Sep 17 00:00:00 2001 From: SeungWon Date: Tue, 7 Nov 2023 23:49:12 +0900 Subject: [PATCH 05/10] refactor: reflect review --- index.js | 4 +--- readme.md | 36 ++++++++++++++++++++++++++++++++++++ test.js | 31 +++++++++++++------------------ 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 2fa5821..3a2f2c6 100644 --- a/index.js +++ b/index.js @@ -27,14 +27,12 @@ const decorateErrorWithCounts = (error, attemptNumber, options) => { return error; }; -const shouldRetry = () => true; - export default async function pRetry(input, options) { return new Promise((resolve, reject) => { options = { onFailedAttempt() {}, retries: 10, - shouldRetry, + shouldRetry: () => true, ...options, }; diff --git a/readme.md b/readme.md index 91fccc7..4eb9980 100644 --- a/readme.md +++ b/readme.md @@ -99,6 +99,42 @@ 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. +##### shouldRetry(error) + +Type: `Function` + +Decide if a retry should occur based on the error. Returning true triggers a retry, false aborts with the 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 startTime = new Date(); + +const result = await pRetry(run, { + shouldRetry: (error) => { + const elapsedTime = new Date() - startTime; + if (elapsedTime < 1000) { + return true; + } + console.log(`Attempt ${error.attemptCount} failed.`); + return false; + }, + retries: 30 +}); + +console.log(result); +``` + ##### signal Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) diff --git a/test.js b/test.js index 118e8f8..ac3d461 100644 --- a/test.js +++ b/test.js @@ -286,24 +286,19 @@ test('should retry only when shouldRetry returns true', async t => { let index = 0; - try { - await pRetry( - async () => { - await delay(40); - index++; - const error = index < 3 ? shouldRetryError : customError; - throw error; - }, - { - async shouldRetry(error) { - return error.message === shouldRetryError.message; - }, - retries: 10, - }, - ); - } catch (error) { - t.is(error.message, customError.message); - } + await t.throwsAsync(pRetry(async () => { + await delay(40); + index++; + const error = index < 3 ? shouldRetryError : customError; + throw error; + }, { + async shouldRetry(error) { + return error.message === shouldRetryError.message; + }, + retries: 10, + }), { + is: customError, + }); t.is(index, 3); }); From f89dcb9448b097b1d144fc26ed27f2b7c3aff2f0 Mon Sep 17 00:00:00 2001 From: SeungWon Date: Tue, 19 Dec 2023 01:46:27 +0900 Subject: [PATCH 06/10] docs: Update shouldRetry --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 4eb9980..7de9756 100644 --- a/readme.md +++ b/readme.md @@ -105,6 +105,8 @@ Type: `Function` Decide if a retry should occur based on the error. Returning true triggers a retry, false aborts with the error. +shouldRetry is not called for TypeErrors (except network errors) and AbortError. + ```js import pRetry from 'p-retry'; From 52b26ba48d548a83c3e09085989d167cd2260a3a Mon Sep 17 00:00:00 2001 From: SeungWon Date: Wed, 20 Dec 2023 23:08:33 +0900 Subject: [PATCH 07/10] docs: Update index.d.ts --- index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.d.ts b/index.d.ts index 9da5b8e..ecff73e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -44,6 +44,8 @@ export type Options = { /** Decide if a retry should occur based on the error. Returning true triggers a retry, false aborts with the error. + shouldRetry is not called for TypeErrors (except network errors) and AbortError. + @param error - The error thrown by the input function. @example From e01e03edd8c574146e76cb0eebce95cd7cbd913a Mon Sep 17 00:00:00 2001 From: SeungWon Date: Wed, 20 Dec 2023 23:09:38 +0900 Subject: [PATCH 08/10] fix: typo --- index.d.ts | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index ecff73e..95ebb17 100644 --- a/index.d.ts +++ b/index.d.ts @@ -44,7 +44,7 @@ export type Options = { /** Decide if a retry should occur based on the error. Returning true triggers a retry, false aborts with the error. - shouldRetry is not called for TypeErrors (except network errors) and AbortError. + It is not called for `TypeError` (except network errors) and `AbortError`. @param error - The error thrown by the input function. diff --git a/readme.md b/readme.md index 7de9756..e1d6fac 100644 --- a/readme.md +++ b/readme.md @@ -105,7 +105,7 @@ Type: `Function` Decide if a retry should occur based on the error. Returning true triggers a retry, false aborts with the error. -shouldRetry is not called for TypeErrors (except network errors) and AbortError. +It is not called for `TypeError` (except network errors) and `AbortError`. ```js import pRetry from 'p-retry'; From 4fb170e1e38b3a9853832cbccc2ad7d456e52862 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 20 Dec 2023 21:34:24 +0100 Subject: [PATCH 09/10] Update readme.md --- readme.md | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/readme.md b/readme.md index e1d6fac..c027fd7 100644 --- a/readme.md +++ b/readme.md @@ -110,31 +110,11 @@ It is not called for `TypeError` (except network errors) and `AbortError`. ```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 startTime = new Date(); +const run = async () => { … }; const result = await pRetry(run, { - shouldRetry: (error) => { - const elapsedTime = new Date() - startTime; - if (elapsedTime < 1000) { - return true; - } - console.log(`Attempt ${error.attemptCount} failed.`); - return false; - }, - retries: 30 + shouldRetry: error => !(error instanceof CustomError); }); - -console.log(result); ``` ##### signal From f0391208890e5ad11dac4629963de449e8dbeda0 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 20 Dec 2023 21:34:55 +0100 Subject: [PATCH 10/10] Update readme.md --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index c027fd7..a12566d 100644 --- a/readme.md +++ b/readme.md @@ -117,6 +117,8 @@ const result = await pRetry(run, { }); ``` +In the example above, the operation will be retried unless the error is an instance of `CustomError`. + ##### signal Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)