import { logger } from './logger'; interface RetryOptions { maxAttempts: number; baseDelayMs?: number; maxDelayMs?: number; retryIf?: (error: unknown) => boolean; label?: string; } /** * Retry a function with exponential backoff. * Returns the result on success or throws after all attempts fail. */ export async function retry( fn: () => Promise, options: RetryOptions ): Promise { const { maxAttempts, baseDelayMs = 1000, maxDelayMs = 30000, retryIf, label = 'operation', } = options; let lastError: unknown; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (retryIf && !retryIf(error)) { throw error; } if (attempt === maxAttempts) { break; } const delay = Math.min(baseDelayMs * Math.pow(2, attempt - 1), maxDelayMs); const jitter = delay * (0.5 + Math.random() * 0.5); logger.warn(`${label} attempt ${attempt}/${maxAttempts} failed, retrying in ${Math.round(jitter)}ms`, { error: error instanceof Error ? error.message : String(error), attempt, maxAttempts, }); await sleep(jitter); } } throw lastError; } /** Promise-based sleep */ export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); }