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); }