/** * Timeout Detection & Recovery * * Detects timeout errors from various sources (fetch, Supabase, edge functions) * and provides recovery strategies. * * Part of Sacred Pipeline Phase 4: Transaction Resilience */ import { logger } from './logger'; export interface TimeoutError extends Error { isTimeout: true; source: 'fetch' | 'supabase' | 'edge-function' | 'database' | 'unknown'; originalError?: unknown; duration?: number; } /** * Check if an error is a timeout error */ export function isTimeoutError(error: unknown): boolean { if (!error) return false; // Check for AbortController timeout if (error instanceof DOMException && error.name === 'AbortError') { return true; } // Check for fetch timeout if (error instanceof TypeError && error.message.includes('aborted')) { return true; } // Check error message for timeout keywords if (error instanceof Error) { const message = error.message.toLowerCase(); return ( message.includes('timeout') || message.includes('timed out') || message.includes('deadline exceeded') || message.includes('request aborted') || message.includes('etimedout') ); } // Check Supabase/HTTP timeout status codes if (error && typeof error === 'object') { const errorObj = error as { status?: number; code?: string; message?: string }; // HTTP 408 Request Timeout if (errorObj.status === 408) return true; // HTTP 504 Gateway Timeout if (errorObj.status === 504) return true; // Supabase timeout codes if (errorObj.code === 'PGRST301') return true; // Connection timeout if (errorObj.code === '57014') return true; // PostgreSQL query cancelled // Check message if (errorObj.message?.toLowerCase().includes('timeout')) return true; } return false; } /** * Wrap an error as a TimeoutError with source information */ export function wrapAsTimeoutError( error: unknown, source: TimeoutError['source'], duration?: number ): TimeoutError { const message = error instanceof Error ? error.message : 'Operation timed out'; const timeoutError = new Error(message) as TimeoutError; timeoutError.name = 'TimeoutError'; timeoutError.isTimeout = true; timeoutError.source = source; timeoutError.originalError = error; timeoutError.duration = duration; return timeoutError; } /** * Execute a function with a timeout wrapper * * @param fn - Function to execute * @param timeoutMs - Timeout in milliseconds * @param source - Source identifier for error tracking * @returns Promise that resolves or rejects with timeout */ export async function withTimeout( fn: () => Promise, timeoutMs: number, source: TimeoutError['source'] = 'unknown' ): Promise { const startTime = Date.now(); const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, timeoutMs); try { // Execute the function with abort signal if supported const result = await fn(); clearTimeout(timeoutId); return result; } catch (error) { clearTimeout(timeoutId); const duration = Date.now() - startTime; // Check if error is timeout-related if (isTimeoutError(error) || controller.signal.aborted) { const timeoutError = wrapAsTimeoutError(error, source, duration); logger.error('Operation timed out', { source, duration, timeoutMs, originalError: error instanceof Error ? error.message : String(error) }); throw timeoutError; } // Re-throw non-timeout errors throw error; } } /** * Categorize timeout severity for recovery strategy */ export function getTimeoutSeverity(error: TimeoutError): 'minor' | 'moderate' | 'critical' { const { duration, source } = error; // No duration means immediate abort - likely user action or critical failure if (!duration) return 'critical'; // Database/edge function timeouts are more critical if (source === 'database' || source === 'edge-function') { if (duration > 30000) return 'critical'; // >30s if (duration > 10000) return 'moderate'; // >10s return 'minor'; } // Fetch timeouts if (source === 'fetch') { if (duration > 60000) return 'critical'; // >60s if (duration > 20000) return 'moderate'; // >20s return 'minor'; } return 'moderate'; } /** * Get recommended retry strategy based on timeout error */ export function getTimeoutRetryStrategy(error: TimeoutError): { shouldRetry: boolean; delayMs: number; maxAttempts: number; increaseTimeout: boolean; } { const severity = getTimeoutSeverity(error); switch (severity) { case 'minor': return { shouldRetry: true, delayMs: 1000, maxAttempts: 3, increaseTimeout: false, }; case 'moderate': return { shouldRetry: true, delayMs: 3000, maxAttempts: 2, increaseTimeout: true, // Increase timeout by 50% }; case 'critical': return { shouldRetry: false, // Don't auto-retry critical timeouts delayMs: 5000, maxAttempts: 1, increaseTimeout: true, }; } } /** * User-friendly timeout error message */ export function getTimeoutErrorMessage(error: TimeoutError): string { const severity = getTimeoutSeverity(error); switch (severity) { case 'minor': return 'The request took longer than expected. Retrying...'; case 'moderate': return 'The server is taking longer than usual to respond. Please wait while we retry.'; case 'critical': return 'The operation timed out. Please check your connection and try again.'; } }