export interface RetryOptions { attempts?: number delay?: number factor?: number maxDelay?: number retryIf?: (error: unknown) => boolean } const TRANSIENT_MESSAGES = [ "load failed", "network connection was lost", "network request failed", "failed to fetch", "econnreset", "econnrefused", "etimedout", "socket hang up", ] function isTransientError(error: unknown): boolean { if (!error) return false const message = String(error instanceof Error ? error.message : error).toLowerCase() return TRANSIENT_MESSAGES.some((m) => message.includes(m)) } export async function retry(fn: () => Promise, options: RetryOptions = {}): Promise { const { attempts = 3, delay = 500, factor = 2, maxDelay = 10000, retryIf = isTransientError } = options let lastError: unknown for (let attempt = 0; attempt < attempts; attempt++) { try { return await fn() } catch (error) { lastError = error if (attempt === attempts - 1 || !retryIf(error)) throw error const wait = Math.min(delay * Math.pow(factor, attempt), maxDelay) await new Promise((resolve) => setTimeout(resolve, wait)) } } throw lastError }