export interface RetryOptions { maxRetries: number; baseDelayMs: number; maxDelayMs: number; jitterFactor: number; retryableErrors: RegExp[]; } const DEFAULT_RETRY_OPTIONS: RetryOptions = { maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 10000, jitterFactor: 0.3, retryableErrors: [ /timeout/i, /ECONNRESET/, /ETIMEDOUT/, /ENOTFOUND/, /exit code 1/, /signal/, /killed/ ] }; export async function withRetry( fn: () => Promise, options?: Partial ): Promise { const opts = { ...DEFAULT_RETRY_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)); const isRetryable = opts.retryableErrors.some( (pattern) => pattern.test(lastError!.message) ); if (!isRetryable || attempt === opts.maxRetries - 1) { throw lastError; } const baseDelay = opts.baseDelayMs * Math.pow(2, attempt); const jitter = Math.random() * opts.jitterFactor * baseDelay; const delay = Math.min(baseDelay + jitter, opts.maxDelayMs); console.warn( `[retry] Attempt ${attempt + 1}/${opts.maxRetries} failed, retrying in ${Math.round(delay)}ms: ${lastError.message}` ); await Bun.sleep(delay); } } throw lastError; }