export interface RetryOptions { maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; backoffMultiplier?: number; shouldRetry?: (error: Error, attempt: number) => boolean; } const DEFAULT_OPTIONS: Required = { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 30000, backoffMultiplier: 2, shouldRetry: () => true, }; /** * Execute a function with exponential backoff retry logic */ export async function withRetry( fn: () => Promise, options: RetryOptions = {} ): Promise { const opts = { ...DEFAULT_OPTIONS, ...options }; let lastError: Error | undefined; for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt >= opts.maxAttempts || !opts.shouldRetry(lastError, attempt)) { throw lastError; } const delay = Math.min( opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1), opts.maxDelayMs ); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw lastError || new Error('Retry failed'); } /** * Determine if an error is transient and should be retried */ export function isTransientError(error: Error): boolean { const message = error.message.toLowerCase(); // Network errors if (message.includes('econnreset') || message.includes('enotfound') || message.includes('etimedout') || message.includes('network')) { return true; } // HTTP status codes that should be retried if (message.includes('429') || // Too Many Requests message.includes('500') || // Internal Server Error message.includes('502') || // Bad Gateway message.includes('503') || // Service Unavailable message.includes('504')) { // Gateway Timeout return true; } return false; } export default withRetry;