import { createLogger } from './logger.js'; const logger = createLogger('Retry'); export interface RetryOptions { maxAttempts?: number; initialDelay?: number; maxDelay?: number; backoffMultiplier?: number; shouldRetry?: (error: Error, attempt: number) => boolean; } const DEFAULT_OPTIONS: Required = { maxAttempts: 3, initialDelay: 1000, maxDelay: 10000, backoffMultiplier: 2, shouldRetry: (error: Error) => { // Retry on network errors and 5xx server errors const message = error.message.toLowerCase(); return ( message.includes('econnrefused') || message.includes('etimedout') || message.includes('enotfound') || message.includes('econnreset') || message.includes('socket hang up') || (message.includes('50') && !message.includes('40')) // 5xx errors ); }, }; /** * Retry a function with exponential backoff */ export async function withRetry(fn: () => Promise, options: RetryOptions = {}): Promise { const opts = { ...DEFAULT_OPTIONS, ...options }; let lastError: Error | null = null; for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; if (attempt === opts.maxAttempts || !opts.shouldRetry(lastError, attempt)) { throw lastError; } const delay = Math.min( opts.initialDelay * Math.pow(opts.backoffMultiplier, attempt - 1), opts.maxDelay ); logger.warn(`Attempt ${attempt} failed, retrying in ${delay}ms`, { error: lastError.message, attempt, nextDelay: delay, }); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw new Error(`Max retry attempts reached: ${lastError?.message}`); } /** * Create a retryable version of a function */ export function makeRetryable( fn: (...args: T) => Promise, options: RetryOptions = {} ): (...args: T) => Promise { return async (...args: T): Promise => { return withRetry(() => fn(...args), options); }; }