import { Result } from "~/lib/result.ts"; export interface RetryOptions { maxAttempts?: number; baseDelayMs?: number; maxDelayMs?: number; retryIf?: (error: unknown, attempt: number) => boolean; onRetry?: (attempt: number, delayMs: number, error: unknown) => void; } const defaults: Required> = { maxAttempts: 4, baseDelayMs: 500, maxDelayMs: 15_000, retryIf: () => true, }; /** delay ∈ {0, ..., ⌊min(cap, base * 2^attempt)βŒ‹ βˆ’ 1}, uniformly distributed */ function jitteredDelay(attempt: number, base: number, cap: number): number { const ceiling = Math.min(cap, base * (2 ** attempt)); return Math.floor(Math.random() * ceiling); } export function withRetryR( fn: () => Promise>, opts: RetryOptions = {}, ): Promise { return withRetry(() => fn().then((result) => { if (!result.ok) throw result.error; return result.value; }), opts); } export async function withRetry( fn: () => Promise, opts: RetryOptions = {}, ): Promise { const { maxAttempts = defaults.maxAttempts, baseDelayMs = defaults.baseDelayMs, maxDelayMs = defaults.maxDelayMs, retryIf = defaults.retryIf, onRetry, } = opts; let lastError: unknown; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (attempt === maxAttempts || !retryIf(error, attempt)) { throw error; } const delayMs = jitteredDelay(attempt, baseDelayMs, maxDelayMs); onRetry?.(attempt, delayMs, error); await sleep(delayMs); } } // unreachable πŸ₯± throw lastError; } export const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms));