feat: complete Ancestor quote bot with production-ready Docker support
This commit is contained in:
298
node_modules/p-retry/index.js
generated
vendored
298
node_modules/p-retry/index.js
generated
vendored
@@ -1,52 +1,14 @@
|
||||
import isNetworkError from 'is-network-error';
|
||||
'use strict';
|
||||
const retry = require('retry');
|
||||
|
||||
function validateRetries(retries) {
|
||||
if (typeof retries === 'number') {
|
||||
if (retries < 0) {
|
||||
throw new TypeError('Expected `retries` to be a non-negative number.');
|
||||
}
|
||||
const networkErrorMsgs = [
|
||||
'Failed to fetch', // Chrome
|
||||
'NetworkError when attempting to fetch resource.', // Firefox
|
||||
'The Internet connection appears to be offline.', // Safari
|
||||
'Network request failed' // `cross-fetch`
|
||||
];
|
||||
|
||||
if (Number.isNaN(retries)) {
|
||||
throw new TypeError('Expected `retries` to be a valid number or Infinity, got NaN.');
|
||||
}
|
||||
|
||||
if (Number.isFinite(retries) && !Number.isInteger(retries)) {
|
||||
throw new TypeError('Expected `retries` to be a non-negative integer or Infinity.');
|
||||
}
|
||||
} 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}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateFunctionOption(name, value) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value !== 'function') {
|
||||
throw new TypeError(`Expected \`${name}\` to be a function.`);
|
||||
}
|
||||
}
|
||||
|
||||
export class AbortError extends Error {
|
||||
class AbortError extends Error {
|
||||
constructor(message) {
|
||||
super();
|
||||
|
||||
@@ -63,209 +25,61 @@ export class AbortError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function calculateDelay(retriesConsumed, options) {
|
||||
const attempt = Math.max(1, retriesConsumed + 1);
|
||||
const random = options.randomize ? (Math.random() + 1) : 1;
|
||||
const decorateErrorWithCounts = (error, attemptNumber, options) => {
|
||||
// Minus 1 from attemptNumber because the first attempt does not count as a retry
|
||||
const retriesLeft = options.retries - (attemptNumber - 1);
|
||||
|
||||
let timeout = Math.round(random * options.minTimeout * (options.factor ** (attempt - 1)));
|
||||
timeout = Math.min(timeout, options.maxTimeout);
|
||||
error.attemptNumber = attemptNumber;
|
||||
error.retriesLeft = retriesLeft;
|
||||
return error;
|
||||
};
|
||||
|
||||
return timeout;
|
||||
}
|
||||
const isNetworkError = errorMessage => networkErrorMsgs.includes(errorMessage);
|
||||
|
||||
function calculateRemainingTime(start, max) {
|
||||
if (!Number.isFinite(max)) {
|
||||
return max;
|
||||
}
|
||||
const pRetry = (input, options) => new Promise((resolve, reject) => {
|
||||
options = {
|
||||
onFailedAttempt: () => {},
|
||||
retries: 10,
|
||||
...options
|
||||
};
|
||||
|
||||
return max - (performance.now() - start);
|
||||
}
|
||||
|
||||
async function delayForRetry(delay, options) {
|
||||
if (delay <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}, delay);
|
||||
|
||||
if (options.unref) {
|
||||
timeoutToken.unref?.();
|
||||
}
|
||||
|
||||
options.signal?.addEventListener('abort', onAbort, {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
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 delayTime = calculateDelay(retriesConsumed, options);
|
||||
const remainingTimeBeforeCallbacks = calculateRemainingTime(startTime, maxRetryTime);
|
||||
|
||||
if (remainingTimeBeforeCallbacks <= 0) {
|
||||
const context = Object.freeze({
|
||||
error: normalizedError,
|
||||
attemptNumber,
|
||||
retriesLeft,
|
||||
retriesConsumed,
|
||||
retryDelay: 0,
|
||||
});
|
||||
|
||||
await options.onFailedAttempt(context);
|
||||
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
const consumeRetryContext = Object.freeze({
|
||||
error: normalizedError,
|
||||
attemptNumber,
|
||||
retriesLeft,
|
||||
retriesConsumed,
|
||||
retryDelay: retriesLeft > 0 ? delayTime : 0,
|
||||
});
|
||||
|
||||
const consumeRetry = await options.shouldConsumeRetry(consumeRetryContext);
|
||||
const effectiveDelay = consumeRetry && retriesLeft > 0 ? delayTime : 0;
|
||||
const context = Object.freeze({
|
||||
error: normalizedError,
|
||||
attemptNumber,
|
||||
retriesLeft,
|
||||
retriesConsumed,
|
||||
retryDelay: effectiveDelay,
|
||||
});
|
||||
|
||||
await options.onFailedAttempt(context);
|
||||
|
||||
if (calculateRemainingTime(startTime, maxRetryTime) <= 0) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
const remainingTime = calculateRemainingTime(startTime, maxRetryTime);
|
||||
|
||||
if (remainingTime <= 0 || retriesLeft <= 0) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
if (normalizedError instanceof TypeError && !isNetworkError(normalizedError)) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
if (!await options.shouldRetry(context)) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
const remainingTimeAfterShouldRetry = calculateRemainingTime(startTime, maxRetryTime);
|
||||
|
||||
if (remainingTimeAfterShouldRetry <= 0) {
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
if (!consumeRetry) {
|
||||
options.signal?.throwIfAborted();
|
||||
return false;
|
||||
}
|
||||
|
||||
const finalDelay = Math.min(effectiveDelay, remainingTimeAfterShouldRetry);
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
|
||||
await delayForRetry(finalDelay, options);
|
||||
|
||||
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
|
||||
validateFunctionOption('onFailedAttempt', options.onFailedAttempt);
|
||||
validateFunctionOption('shouldRetry', options.shouldRetry);
|
||||
validateFunctionOption('shouldConsumeRetry', options.shouldConsumeRetry);
|
||||
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++;
|
||||
const operation = retry.operation(options);
|
||||
|
||||
operation.attempt(async attemptNumber => {
|
||||
try {
|
||||
options.signal?.throwIfAborted();
|
||||
|
||||
const result = await input(attemptNumber);
|
||||
|
||||
options.signal?.throwIfAborted();
|
||||
|
||||
return result;
|
||||
resolve(await input(attemptNumber));
|
||||
} catch (error) {
|
||||
if (await onAttemptFailure({
|
||||
error,
|
||||
attemptNumber,
|
||||
retriesConsumed,
|
||||
startTime,
|
||||
options,
|
||||
})) {
|
||||
retriesConsumed++;
|
||||
if (!(error instanceof Error)) {
|
||||
reject(new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof AbortError) {
|
||||
operation.stop();
|
||||
reject(error.originalError);
|
||||
} else if (error instanceof TypeError && !isNetworkError(error.message)) {
|
||||
operation.stop();
|
||||
reject(error);
|
||||
} else {
|
||||
decorateErrorWithCounts(error, attemptNumber, options);
|
||||
|
||||
try {
|
||||
await options.onFailedAttempt(error);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!operation.retry(error)) {
|
||||
reject(operation.mainError());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Should not reach here, but in case it does, throw an error
|
||||
throw new Error('Retry attempts exhausted without throwing an error.');
|
||||
}
|
||||
module.exports = pRetry;
|
||||
// TODO: remove this in the next major version
|
||||
module.exports.default = pRetry;
|
||||
|
||||
export function makeRetriable(function_, options) {
|
||||
return function (...arguments_) {
|
||||
return pRetry(() => function_.apply(this, arguments_), options);
|
||||
};
|
||||
}
|
||||
module.exports.AbortError = AbortError;
|
||||
|
||||
Reference in New Issue
Block a user