import { ProcessingError } from '../types/processing'; export class ProcessingErrorHandler { private static readonly DEFAULT_MAX_RETRIES = 3; private static readonly RETRY_DELAYS = [1000, 2000, 4000]; // Progressive backoff in ms /** * Create a ProcessingError from an unknown error */ static createProcessingError( error: unknown, retryCount: number = 0, maxRetries: number = this.DEFAULT_MAX_RETRIES ): ProcessingError { const originalError = error instanceof Error ? error : new Error(String(error)); const message = originalError.message; // Determine error type based on error message and properties const errorType = this.determineErrorType(originalError, message); // Determine if error is recoverable const recoverable = this.isRecoverable(errorType, retryCount, maxRetries); return { type: errorType, message: this.formatErrorMessage(errorType, message), recoverable, retryCount, maxRetries, originalError }; } /** * Determine the type of error based on error characteristics */ private static determineErrorType(error: Error, message: string): ProcessingError['type'] { const lowerMessage = message.toLowerCase(); // Network-related errors if (lowerMessage.includes('network') || lowerMessage.includes('fetch') || lowerMessage.includes('connection')) { return 'network'; } // Memory-related errors if (lowerMessage.includes('memory') || lowerMessage.includes('quota') || lowerMessage.includes('allocation') || error.name === 'QuotaExceededError') { return 'memory'; } // Timeout errors if (lowerMessage.includes('timeout') || lowerMessage.includes('aborted') || error.name === 'AbortError') { return 'timeout'; } // Cancellation if (lowerMessage.includes('cancel') || lowerMessage.includes('abort') || error.name === 'AbortError') { return 'cancelled'; } // PDF corruption/parsing errors if (lowerMessage.includes('pdf') || lowerMessage.includes('parse') || lowerMessage.includes('invalid') || lowerMessage.includes('corrupt') || lowerMessage.includes('malformed')) { return 'corruption'; } // Default to parsing error return 'parsing'; } /** * Determine if an error is recoverable based on type and retry count */ private static isRecoverable( errorType: ProcessingError['type'], retryCount: number, maxRetries: number ): boolean { // Never recoverable if (errorType === 'cancelled' || errorType === 'corruption') { return false; } // Recoverable if we haven't exceeded retry count if (retryCount >= maxRetries) { return false; } // Memory errors are usually not recoverable if (errorType === 'memory') { return retryCount < 1; // Only one retry for memory errors } // Network and timeout errors are usually recoverable return errorType === 'network' || errorType === 'timeout' || errorType === 'parsing'; } /** * Format error message for user display */ private static formatErrorMessage(errorType: ProcessingError['type'], originalMessage: string): string { switch (errorType) { case 'network': return 'Network connection failed. Please check your internet connection and try again.'; case 'memory': return 'Insufficient memory to process this file. Try closing other applications or processing a smaller file.'; case 'timeout': return 'Processing timed out. This file may be too large or complex to process.'; case 'cancelled': return 'Processing was cancelled by user.'; case 'corruption': return 'This PDF file appears to be corrupted or encrypted. Please try a different file.'; case 'parsing': return `Failed to process PDF: ${originalMessage}`; default: return `Processing failed: ${originalMessage}`; } } /** * Execute an operation with automatic retry logic */ static async executeWithRetry( operation: () => Promise, onError?: (error: ProcessingError) => void, maxRetries: number = this.DEFAULT_MAX_RETRIES ): Promise { let lastError: ProcessingError | null = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = this.createProcessingError(error, attempt, maxRetries); // Notify error handler if (onError) { onError(lastError); } // Don't retry if not recoverable if (!lastError.recoverable) { break; } // Don't retry on last attempt if (attempt === maxRetries) { break; } // Wait before retry with progressive backoff const delay = this.RETRY_DELAYS[Math.min(attempt, this.RETRY_DELAYS.length - 1)]; await this.delay(delay); console.log(`Retrying operation (attempt ${attempt + 2}/${maxRetries + 1}) after ${delay}ms delay`); } } // All retries exhausted throw lastError || new Error('Operation failed after all retries'); } /** * Create a timeout wrapper for operations */ static withTimeout( operation: () => Promise, timeoutMs: number, timeoutMessage: string = 'Operation timed out' ): Promise { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(timeoutMessage)); }, timeoutMs); operation() .then(result => { clearTimeout(timeoutId); resolve(result); }) .catch(error => { clearTimeout(timeoutId); reject(error); }); }); } /** * Create an AbortController that times out after specified duration */ static createTimeoutController(timeoutMs: number): AbortController { const controller = new AbortController(); setTimeout(() => { controller.abort(); }, timeoutMs); return controller; } /** * Check if an error indicates the operation should be retried */ static shouldRetry(error: ProcessingError): boolean { return error.recoverable && error.retryCount < error.maxRetries; } /** * Get user-friendly suggestions based on error type */ static getErrorSuggestions(error: ProcessingError): string[] { switch (error.type) { case 'network': return [ 'Check your internet connection', 'Try refreshing the page', 'Try again in a few moments' ]; case 'memory': return [ 'Close other browser tabs or applications', 'Try processing a smaller file', 'Restart your browser', 'Use a device with more memory' ]; case 'timeout': return [ 'Try processing a smaller file', 'Break large files into smaller sections', 'Check your internet connection speed' ]; case 'corruption': return [ 'Verify the PDF file opens in other applications', 'Try re-downloading the file', 'Try a different PDF file', 'Contact the file creator if it appears corrupted' ]; case 'parsing': return [ 'Verify this is a valid PDF file', 'Try a different PDF file', 'Contact support if the problem persists' ]; default: return [ 'Try refreshing the page', 'Try again in a few moments', 'Contact support if the problem persists' ]; } } /** * Utility function for delays */ private static delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } }