1type RetryContext = {2 attempt: number;3 signal: AbortSignal;4};56type RetryOptions = {7 maxAttempts: number;8 baseDelayMs: number;9 maxDelayMs: number;10 factor?: number;11 perAttemptTimeoutMs: number;12 overallTimeoutMs: number;13 signal?: AbortSignal;14 random?: () => number;15 isRetryable: (error: unknown) => boolean;16 onRetry?: (event: {17 error: unknown;18 attempt: number;19 delayMs: number;20 }) => void;21};2223async function retry<T>(24 operation: (context: RetryContext) => Promise<T>,25 options: RetryOptions,26): Promise<T> {27 const {28 maxAttempts,29 baseDelayMs,30 maxDelayMs,31 factor = 2,32 perAttemptTimeoutMs,33 overallTimeoutMs,34 signal,35 random = Math.random,36 isRetryable,37 onRetry,38 } = options;3940 if (maxAttempts < 1) throw new Error("maxAttempts must be >= 1");4142 const startedAt = performance.now();43 let lastError: unknown;4445 for (let attempt = 1; attempt <= maxAttempts; attempt++) {46 if (signal?.aborted) throw signal.reason;4748 const elapsed = performance.now() - startedAt;49 const remaining = overallTimeoutMs - elapsed;50 if (remaining <= 0) throw lastError ?? new Error("Retry deadline exceeded");5152 const controller = new AbortController();53 const forwardAbort = () => controller.abort(signal?.reason);54 signal?.addEventListener("abort", forwardAbort, { once: true });5556 const timeoutMs = Math.min(perAttemptTimeoutMs, remaining);57 const timer = setTimeout(58 () => controller.abort(new Error("Attempt timeout")),59 timeoutMs,60 );6162 try {63 return await operation({ attempt, signal: controller.signal });64 } catch (error) {65 lastError = error;6667 if (68 signal?.aborted ||69 attempt === maxAttempts ||70 !isRetryable(error)71 ) {72 throw error;73 }74 } finally {75 clearTimeout(timer);76 signal?.removeEventListener("abort", forwardAbort);77 }7879 const retryIndex = attempt - 1;80 const cap = exponentialCap(81 retryIndex,82 baseDelayMs,83 maxDelayMs,84 factor,85 );86 const delayMs = fullJitterDelay(cap, random);8788 const remainingAfterAttempt =89 overallTimeoutMs - (performance.now() - startedAt);90 if (delayMs >= remainingAfterAttempt) throw lastError;9192 onRetry?.({ error: lastError, attempt, delayMs });93 await sleep(delayMs, signal);94 }9596 throw lastError;97}