Skip to content

Commit

Permalink
Rewrite the package to not depend on retry
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Oct 12, 2024
1 parent 21a22dd commit 4acb783
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 63 deletions.
152 changes: 92 additions & 60 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import retry from 'retry';
import isNetworkError from 'is-network-error';

export class AbortError extends Error {
Expand Down Expand Up @@ -27,68 +26,101 @@ const decorateErrorWithCounts = (error, attemptNumber, options) => {
return error;
};

export default async function pRetry(input, options) {
return new Promise((resolve, reject) => {
options = {
onFailedAttempt() {},
retries: 10,
shouldRetry: () => true,
...options,
};
function calculateDelay(attempt, options) {
const random = options.randomize ? (Math.random() + 1) : 1;

const operation = retry.operation(options);
let timeout = Math.round(random * Math.max(options.minTimeout, 1) * (options.factor ** (attempt - 1)));
timeout = Math.min(timeout, options.maxTimeout);

const abortHandler = () => {
operation.stop();
reject(options.signal?.reason);
};
return timeout;
}

if (options.signal && !options.signal.aborted) {
options.signal.addEventListener('abort', abortHandler, {once: true});
}
export default async function pRetry(input, options = {}) {
options = {
retries: 10,
factor: 2,
minTimeout: 1000,
maxTimeout: Number.POSITIVE_INFINITY,
randomize: false,
onFailedAttempt() {},
shouldRetry: () => true,
...options,
};

options.signal?.throwIfAborted();

let attemptNumber = 0;
const startTime = Date.now();

const maxRetryTime = options.maxRetryTime ?? Number.POSITIVE_INFINITY;

while (attemptNumber < options.retries + 1) {
attemptNumber++;

try {
options.signal?.throwIfAborted();

const result = await input(attemptNumber);

const cleanUp = () => {
options.signal?.removeEventListener('abort', abortHandler);
operation.stop();
};

operation.attempt(async attemptNumber => {
try {
const result = await input(attemptNumber);
cleanUp();
resolve(result);
} catch (error) {
try {
if (!(error instanceof Error)) {
throw new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
}

if (error instanceof AbortError) {
throw error.originalError;
}

if (error instanceof TypeError && !isNetworkError(error)) {
throw error;
}

decorateErrorWithCounts(error, attemptNumber, options);

if (!(await options.shouldRetry(error))) {
operation.stop();
reject(error);
}

await options.onFailedAttempt(error);

if (!operation.retry(error)) {
throw operation.mainError();
}
} catch (finalError) {
decorateErrorWithCounts(finalError, attemptNumber, options);
cleanUp();
reject(finalError);
}
options.signal?.throwIfAborted();

return result;
} catch (catchError) {
let error = catchError;

if (!(error instanceof Error)) {
error = new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
}

if (error instanceof AbortError) {
throw error.originalError;
}
});
});

if (error instanceof TypeError && !isNetworkError(error)) {
throw error;
}

decorateErrorWithCounts(error, attemptNumber, options);

// Always call onFailedAttempt
await options.onFailedAttempt(error);

const currentTime = Date.now();
if (
currentTime - startTime >= maxRetryTime
|| attemptNumber >= options.retries + 1
|| !(await options.shouldRetry(error))
) {
throw error; // Do not retry, throw the original error
}

// Calculate delay before next attempt
const delayTime = calculateDelay(attemptNumber, options);

// Ensure that delay does not exceed maxRetryTime
const timeLeft = maxRetryTime - (currentTime - startTime);
if (timeLeft <= 0) {
throw error; // Max retry time exceeded
}

const finalDelay = Math.min(delayTime, timeLeft);

// Introduce delay
if (finalDelay > 0) {
await new Promise((resolve, reject) => {
const timeoutToken = setTimeout(resolve, finalDelay);

options.signal?.addEventListener('abort', () => {
clearTimeout(timeoutToken);
reject(options.signal.reason);
}, {once: true});
});
}

options.signal?.throwIfAborted();
}
}

// Should not reach here, but in case it does, throw an error
throw new Error('Retry attempts exhausted without throwing an error.');
}
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@
"bluebird"
],
"dependencies": {
"@types/retry": "0.12.2",
"is-network-error": "^1.0.0",
"retry": "^0.13.1"
"is-network-error": "^1.0.0"
},
"devDependencies": {
"ava": "^5.3.1",
"delay": "^6.0.0",
"tsd": "^0.28.1",
"xo": "^0.56.0"
},
"xo": {
"rules": {
"no-await-in-loop": "off"
}
}
}

0 comments on commit 4acb783

Please sign in to comment.