/** * Retry logic utilities */ import { isRetryableError } from './error.js'; /** * Retry options */ export interface RetryOptions { /** Maximum number of retry attempts */ maxAttempts?: number; /** Initial delay in milliseconds */ initialDelay?: number; /** Maximum delay in milliseconds */ maxDelay?: number; /** Backoff multiplier */ backoffMultiplier?: number; /** Custom retry condition */ shouldRetry?: (error: unknown, attempt: number) => boolean; } /** * Default retry options */ export const DEFAULT_RETRY_OPTIONS: Required = { maxAttempts: 3, initialDelay: 1000, maxDelay: 10000, backoffMultiplier: 2, shouldRetry: isRetryableError, }; /** * Delay for a specified time */ export function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Calculate delay with exponential backoff */ export function calculateBackoff( attempt: number, initialDelay: number, maxDelay: number, multiplier: number ): number { const exponentialDelay = initialDelay * Math.pow(multiplier, attempt - 1); return Math.min(exponentialDelay, maxDelay); } /** * Retry a function with exponential backoff */ export async function withRetry( fn: () => Promise, options: RetryOptions = {} ): Promise { const opts = { ...DEFAULT_RETRY_OPTIONS, ...options }; let lastError: unknown; for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Check if we should retry const shouldRetry = opts.shouldRetry(error, attempt); const hasAttemptsLeft = attempt < opts.maxAttempts; if (!shouldRetry || !hasAttemptsLeft) { throw error; } // Calculate delay and wait const delayMs = calculateBackoff( attempt, opts.initialDelay, opts.maxDelay, opts.backoffMultiplier ); await delay(delayMs); } } // This should never be reached, but TypeScript needs it throw lastError; }