/** * Edge Function Retry Helper * Provides exponential backoff retry logic for external API calls */ import { edgeLogger } from './logger.ts'; export interface EdgeRetryOptions { maxAttempts?: number; baseDelay?: number; maxDelay?: number; backoffMultiplier?: number; jitter?: boolean; shouldRetry?: (error: unknown) => boolean; } /** * Determines if an error is transient and should be retried */ export function isRetryableError(error: unknown): boolean { // Network errors if (error instanceof TypeError && error.message.includes('fetch')) return true; if (error instanceof Error) { const msg = error.message.toLowerCase(); if (msg.includes('network') || msg.includes('timeout') || msg.includes('econnrefused')) { return true; } } // HTTP status codes that should be retried if (error && typeof error === 'object') { const httpError = error as { status?: number }; // Rate limiting if (httpError.status === 429) return true; // Service unavailable or gateway timeout if (httpError.status === 503 || httpError.status === 504) return true; // Server errors (5xx) if (httpError.status && httpError.status >= 500 && httpError.status < 600) { return true; } } return false; } /** * Calculate exponential backoff delay with optional jitter */ function calculateBackoffDelay( attempt: number, baseDelay: number, maxDelay: number, backoffMultiplier: number, jitter: boolean ): number { const exponentialDelay = baseDelay * Math.pow(backoffMultiplier, attempt); const cappedDelay = Math.min(exponentialDelay, maxDelay); if (!jitter) return cappedDelay; // Add random jitter (-30% to +30%) const jitterAmount = cappedDelay * 0.3; const randomJitter = (Math.random() * 2 - 1) * jitterAmount; return Math.max(0, cappedDelay + randomJitter); } /** * Retry wrapper for asynchronous operations with exponential backoff * * @param fn - Async function to retry * @param options - Retry configuration * @param requestId - Request ID for tracking * @param context - Context description for logging */ export async function withEdgeRetry( fn: () => Promise, options: EdgeRetryOptions = {}, requestId: string, context: string ): Promise { const maxAttempts = options.maxAttempts ?? 3; const baseDelay = options.baseDelay ?? 1000; const maxDelay = options.maxDelay ?? 10000; const backoffMultiplier = options.backoffMultiplier ?? 2; const jitter = options.jitter ?? true; const shouldRetry = options.shouldRetry ?? isRetryableError; let lastError: unknown; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Don't retry if this is the last attempt if (attempt === maxAttempts - 1) { edgeLogger.error('All retry attempts exhausted', { requestId, context, attempts: maxAttempts, error: error instanceof Error ? error.message : 'Unknown error' }); throw error; } // Don't retry if error is not retryable if (!shouldRetry(error)) { edgeLogger.info('Error not retryable, failing immediately', { requestId, context, attempt: attempt + 1, error: error instanceof Error ? error.message : 'Unknown error' }); throw error; } // Calculate delay for next retry const delay = calculateBackoffDelay(attempt, baseDelay, maxDelay, backoffMultiplier, jitter); edgeLogger.info('Retrying after error', { requestId, context, attempt: attempt + 1, maxAttempts, delay: Math.round(delay), error: error instanceof Error ? error.message : 'Unknown error' }); // Wait before retrying await new Promise(resolve => setTimeout(resolve, delay)); } } // This should never be reached, but TypeScript needs it throw lastError; }