80 lines
2.2 KiB
JavaScript
80 lines
2.2 KiB
JavaScript
const retryKinds = new Set([
|
|
"NETWORK_UNREACHABLE",
|
|
"REQUEST_TIMEOUT",
|
|
"RATE_LIMITED",
|
|
"SERVER_FAILURE",
|
|
]);
|
|
|
|
/**
|
|
* @param {number} retryIndex
|
|
* @param {() => number} [random]
|
|
* @param {number} [baseDelayMs]
|
|
* @param {number} [maxDelayMs]
|
|
*/
|
|
export function calculateBackoff(
|
|
retryIndex,
|
|
random = Math.random,
|
|
baseDelayMs = 250,
|
|
maxDelayMs = 2_000,
|
|
) {
|
|
return Math.min(maxDelayMs, baseDelayMs * 2 ** retryIndex) * random();
|
|
}
|
|
|
|
/** @param {string | null | undefined} value @param {number} [now] */
|
|
export function parseRetryAfter(value, now = Date.now()) {
|
|
if (!value) return null;
|
|
|
|
const seconds = Number(value);
|
|
if (Number.isFinite(seconds)) {
|
|
return seconds < 0 ? null : seconds * 1_000;
|
|
}
|
|
|
|
const timestamp = Date.parse(value);
|
|
if (!Number.isFinite(timestamp)) return null;
|
|
return Math.max(0, timestamp - now);
|
|
}
|
|
|
|
/**
|
|
* @param {{ idempotency: "safe" | "keyed" | "none" }} operation
|
|
* @param {{ kind: string, retryAfterMs?: number, httpStatus?: number }} failure
|
|
* @param {number} retryCount
|
|
* @param {number} [maxRetries]
|
|
*/
|
|
export function shouldRetry(operation, failure, retryCount, maxRetries = 2) {
|
|
if (retryCount >= maxRetries) return false;
|
|
if (!retryKinds.has(failure.kind)) return false;
|
|
if (
|
|
failure.kind === "SERVER_FAILURE" &&
|
|
failure.httpStatus !== undefined &&
|
|
![502, 503, 504].includes(failure.httpStatus)
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
failure.kind === "RATE_LIMITED" &&
|
|
typeof failure.retryAfterMs === "number" &&
|
|
failure.retryAfterMs > 30_000
|
|
) {
|
|
return false;
|
|
}
|
|
return operation.idempotency === "safe" || operation.idempotency === "keyed";
|
|
}
|
|
|
|
/**
|
|
* @param {{ kind: string, retryAfterMs?: number, retryAfter?: string }} failure
|
|
* @param {number} retryIndex
|
|
* @param {() => number} [random]
|
|
* @param {number} [now]
|
|
*/
|
|
export function retryDelay(failure, retryIndex, random = Math.random, now = Date.now()) {
|
|
const localBackoff = calculateBackoff(retryIndex, random);
|
|
if (failure.kind !== "RATE_LIMITED") return localBackoff;
|
|
|
|
const retryAfterMs =
|
|
typeof failure.retryAfterMs === "number"
|
|
? failure.retryAfterMs
|
|
: parseRetryAfter(failure.retryAfter, now);
|
|
|
|
return retryAfterMs === null ? localBackoff : Math.max(localBackoff, retryAfterMs);
|
|
}
|