/** * Retry utility with exponential backoff */ export interface RetryOptions { maxRetries?: number initialDelay?: number maxDelay?: number backoffMultiplier?: number retryable?: (error: Error) => boolean } const DEFAULT_OPTIONS: Required = { maxRetries: 3, initialDelay: 1000, maxDelay: 10000, backoffMultiplier: 2, retryable: () => true, } /** * Retry a function with exponential backoff */ export async function retryWithBackoff( fn: () => Promise, options: RetryOptions = {} ): Promise { const opts = { ...DEFAULT_OPTIONS, ...options } let lastError: Error | null = null for (let attempt = 0; attempt <= opts.maxRetries; attempt++) { try { return await fn() } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)) // Check if error is retryable if (!opts.retryable(lastError)) { throw lastError } // Don't retry on last attempt if (attempt === opts.maxRetries) { throw lastError } // Calculate delay with exponential backoff const delay = Math.min( opts.initialDelay * Math.pow(opts.backoffMultiplier, attempt), opts.maxDelay ) // Wait before retrying await new Promise((resolve) => setTimeout(resolve, delay)) } } throw lastError || new Error('Retry failed') } /** * Check if an error is a network error that should be retried */ export function isRetryableNetworkError(error: Error): boolean { // Network errors if (error.message.includes('network') || error.message.includes('fetch')) { return true } // Timeout errors if (error.message.includes('timeout') || error.message.includes('timed out')) { return true } // Connection errors if (error.message.includes('ECONNRESET') || error.message.includes('ECONNREFUSED')) { return true } // Rate limiting (429) if (error.message.includes('429') || error.message.includes('rate limit')) { return true } // Server errors (5xx) if (error.message.includes('50')) { return true } return false }