From 756dd7bbf1d0106f64903ca8563de27747c5efa0 Mon Sep 17 00:00:00 2001 From: yutak23 Date: Wed, 31 Jul 2024 16:10:49 +0900 Subject: [PATCH] feat: add linearDelay for retryDelay option Update spec/index.spec.ts Co-authored-by: Alberto Gonzalez Update spec/index.spec.ts Co-authored-by: Alberto Gonzalez --- README.md | 5 ++- spec/index.spec.ts | 77 +++++++++++++++++++++++++++++++++++++++++++++- src/index.ts | 16 ++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 54d263c..06efe8f 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ axios.get('http://example.com/test') // The first request fails and the second r // Exponential back-off retry delay between requests axiosRetry(axios, { retryDelay: axiosRetry.exponentialDelay }); +// Liner retry delay between requests +axiosRetry(axios, { retryDelay: axiosRetry.linearDelay() }); + // Custom retry delay axiosRetry(axios, { retryDelay: (retryCount) => { return retryCount * 1000; @@ -64,7 +67,7 @@ client | retries | `Number` | `3` | The number of times to retry before failing. 1 = One retry after first failure | | retryCondition | `Function` | `isNetworkOrIdempotentRequestError` | A callback to further control if a request should be retried. By default, it retries if it is a network error or a 5xx error on an idempotent request (GET, HEAD, OPTIONS, PUT or DELETE). | | shouldResetTimeout | `Boolean` | false | Defines if the timeout should be reset between retries | -| retryDelay | `Function` | `function noDelay() { return 0; }` | A callback to further control the delay in milliseconds between retried requests. By default there is no delay between retries. Another option is exponentialDelay ([Exponential Backoff](https://developers.google.com/analytics/devguides/reporting/core/v3/errors#backoff)). The function is passed `retryCount` and `error`. | +| retryDelay | `Function` | `function noDelay() { return 0; }` | A callback to further control the delay in milliseconds between retried requests. By default there is no delay between retries. Another option is exponentialDelay ([Exponential Backoff](https://developers.google.com/analytics/devguides/reporting/core/v3/errors#backoff)) or `linearDelay`. The function is passed `retryCount` and `error`. | | onRetry | `Function` | `function onRetry(retryCount, error, requestConfig) { return; }` | A callback to notify when a retry is about to occur. Useful for tracing and you can any async process for example refresh a token on 401. By default nothing will occur. The function is passed `retryCount`, `error`, and `requestConfig`. | | onMaxRetryTimesExceeded | `Function` | `function onMaxRetryTimesExceeded(error, retryCount) { return; }` | After all the retries are failed, this callback will be called with the last error before throwing the error. | | validateResponse | `Function \| null` | `null` | A callback to define whether a response should be resolved or rejected. If null is passed, it will fallback to the axios default (only 2xx status codes are resolved). | diff --git a/spec/index.spec.ts b/spec/index.spec.ts index acd6dc6..c0373c0 100644 --- a/spec/index.spec.ts +++ b/spec/index.spec.ts @@ -8,7 +8,8 @@ import axiosRetry, { exponentialDelay, retryAfter, isRetryableError, - namespace + namespace, + linearDelay } from '../src/index'; const NETWORK_ERROR = new AxiosError('Some connection error'); @@ -614,6 +615,53 @@ describe('axiosRetry(axios, { retries, retryDelay })', () => { }, done.fail); }); }); + + describe('when linearDelay is supplied', () => { + it('should take more than 600 milliseconds with default delay and 4 retries', (done) => { + const client = axios.create(); + setupResponses(client, [ + () => nock('http://example.com').get('/test').replyWithError(NETWORK_ERROR), + () => nock('http://example.com').get('/test').replyWithError(NETWORK_ERROR), + () => nock('http://example.com').get('/test').replyWithError(NETWORK_ERROR), + () => nock('http://example.com').get('/test').reply(200, 'It worked!') + ]); + const timeStart = new Date().getTime(); + axiosRetry(client, { + retries: 4, + retryCondition: () => true, + retryDelay: linearDelay() + }); + client.get('http://example.com/test').then(() => { + // 100 + 200 + 300 = 600 + expect(new Date().getTime() - timeStart).toBeGreaterThanOrEqual(600); + expect(new Date().getTime() - timeStart).toBeLessThan(700); + + done(); + }, done.fail); + }); + + it('should take more than 300 milliseconds with 50ms delay and 4 retries', (done) => { + const client = axios.create(); + setupResponses(client, [ + () => nock('http://example.com').get('/test').replyWithError(NETWORK_ERROR), + () => nock('http://example.com').get('/test').replyWithError(NETWORK_ERROR), + () => nock('http://example.com').get('/test').replyWithError(NETWORK_ERROR), + () => nock('http://example.com').get('/test').reply(200, 'It worked!') + ]); + const timeStart = new Date().getTime(); + axiosRetry(client, { + retries: 4, + retryCondition: () => true, + retryDelay: linearDelay(50) + }); + client.get('http://example.com/test').then(() => { + // 50 + 100 + 150 = 300 + expect(new Date().getTime() - timeStart).toBeGreaterThanOrEqual(300); + expect(new Date().getTime() - timeStart).toBeLessThan(400); + done(); + }, done.fail); + }); + }); }); describe('axiosRetry(axios, { retries, onRetry })', () => { @@ -1054,6 +1102,33 @@ describe('exponentialDelay', () => { }); }); +describe('linearDelay', () => { + it('should return liner retry delay', () => { + const linearDelayFunc = linearDelay(); + + function assertTime(retryNumber) { + const time = linearDelayFunc(retryNumber, undefined); + + expect(time).toBe(100 * retryNumber); + } + + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].forEach(assertTime); + }); + + it('should change delay time when specifying delay factor', () => { + const delayFactor = 300; + const linearDelayFunc = linearDelay(delayFactor); + + function assertTime(retryNumber) { + const time = linearDelayFunc(retryNumber, undefined); + + expect(time).toBe(delayFactor * retryNumber); + } + + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].forEach(assertTime); + }); +}); + describe('retryAfter', () => { it('should understand a numeric Retry-After header', () => { const errorResponse = new AxiosError('Error response'); diff --git a/src/index.ts b/src/index.ts index a72185e..a276b3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -81,6 +81,7 @@ export interface AxiosRetry { isIdempotentRequestError(error: AxiosError): boolean; isNetworkOrIdempotentRequestError(error: AxiosError): boolean; exponentialDelay(retryNumber?: number, error?: AxiosError, delayFactor?: number): number; + linearDelay(delayFactor?: number): (retryNumber: number, error: AxiosError | undefined) => number; } declare module 'axios' { @@ -169,6 +170,20 @@ export function exponentialDelay( return delay + randomSum; } +/** + * Linear delay + * @param {number | undefined} delayFactor - delay factor in milliseconds (default: 100) + * @returns {function} (retryNumber: number, error: AxiosError | undefined) => number + */ +export function linearDelay( + delayFactor: number | undefined = 100 +): (retryNumber: number, error: AxiosError | undefined) => number { + return (retryNumber = 0, error = undefined) => { + const delay = retryNumber * delayFactor; + return Math.max(delay, retryAfter(error)); + }; +} + export const DEFAULT_OPTIONS: Required = { retries: 3, retryCondition: isNetworkOrIdempotentRequestError, @@ -322,5 +337,6 @@ axiosRetry.isSafeRequestError = isSafeRequestError; axiosRetry.isIdempotentRequestError = isIdempotentRequestError; axiosRetry.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError; axiosRetry.exponentialDelay = exponentialDelay; +axiosRetry.linearDelay = linearDelay; axiosRetry.isRetryableError = isRetryableError; export default axiosRetry;