/** * Error Recovery - Phase 8 Enterprise Pattern * * Handles crashes, recovers lost state, and provides * resilience against errors and edge cases. */ import store from '../stores/app-store.js'; import auditLogger from './audit-logger.js'; import persistence from './workflow-persistence.js'; class ErrorRecovery { constructor() { this.errorHandlers = new Map(); this.recoveryPoints = []; this.maxRecoveryPoints = 5; this.setupGlobalErrorHandlers(); } /** * Setup global error handlers */ setupGlobalErrorHandlers() { // Unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { this.handleError(event.reason, 'unhandled_promise'); // Prevent default error handling event.preventDefault(); }); // Global errors window.addEventListener('error', (event) => { this.handleError(event.error, 'global_error'); }); // Log before unload (potential crash) window.addEventListener('beforeunload', () => { persistence.saveSnapshot(); auditLogger.logAction('session_end', { cleanShutdown: true }); }); } /** * Register error handler for specific error type */ registerHandler(errorType, handler) { this.handlers.set(errorType, handler); } /** * Create recovery point */ createRecoveryPoint(label = '') { const point = { id: `recovery-${Date.now()}`, label, timestamp: new Date().toISOString(), snapshot: persistence.snapshot(), logs: auditLogger.getLogs({ limit: 50 }), state: store.get() }; this.recoveryPoints.unshift(point); if (this.recoveryPoints.length > this.maxRecoveryPoints) { this.recoveryPoints.pop(); } auditLogger.logAction('recovery_point_created', { label }); return point.id; } /** * Get recovery points */ getRecoveryPoints() { return this.recoveryPoints; } /** * Recover from recovery point */ recover(pointId) { const point = this.recoveryPoints.find(p => p.id === pointId); if (!point) { auditLogger.logWarning('Recovery point not found', { pointId }); return false; } try { // Restore workflow state persistence.restoreSnapshot(point.snapshot.id); auditLogger.logAction('recovered_from_point', { pointId, label: point.label }); return true; } catch (e) { this.handleError(e, 'recovery_failed'); return false; } } /** * Check if app is in crashed state */ detectCrash() { const lastSnapshot = persistence.getLatestSnapshot(); if (!lastSnapshot) return false; const lastActivityTime = new Date(lastSnapshot.timestamp); const now = new Date(); const timeSinceLastActivity = now - lastActivityTime; // If no activity in last 5 minutes and session started, likely crashed return timeSinceLastActivity > 5 * 60 * 1000; } /** * Main error handler */ handleError(error, context = 'unknown') { const errorId = auditLogger.logError(error, context); // Create recovery point before handling const recoveryId = this.createRecoveryPoint(`Error recovery: ${context}`); // Categorize error const category = this.categorizeError(error); // Apply recovery strategy const recovery = this.getRecoveryStrategy(category); if (recovery) { recovery.execute(error, store, auditLogger); } // Notify user this.notifyUser(error, category, errorId); return { errorId, recoveryId, category }; } /** * Categorize error */ categorizeError(error) { if (error.message.includes('Network')) return 'network'; if (error.message.includes('timeout')) return 'timeout'; if (error.message.includes('Permission')) return 'permission'; if (error.message.includes('Authentication')) return 'auth'; if (error.message.includes('not found')) return 'notfound'; return 'unknown'; } /** * Get recovery strategy for error category */ getRecoveryStrategy(category) { const strategies = { network: { execute: (error, store, logger) => { store.notify('Network error - retrying...', 'warning'); logger.logWarning('Network error detected', { retrying: true }); }, retryable: true }, timeout: { execute: (error, store, logger) => { store.notify('Request timeout - please try again', 'warning'); logger.logWarning('Request timeout', { timeout: true }); }, retryable: true }, auth: { execute: (error, store, logger) => { store.notify('Authentication required - redirecting to login', 'error'); window.location.hash = '#/login'; }, retryable: false }, permission: { execute: (error, store, logger) => { store.notify('Access denied', 'error'); logger.logWarning('Permission denied', { error: error.message }); }, retryable: false }, notfound: { execute: (error, store, logger) => { store.notify('Resource not found', 'warning'); }, retryable: false } }; return strategies[category] || strategies.unknown; } /** * Notify user of error */ notifyUser(error, category, errorId) { const messages = { network: 'Network connection error. Please check your internet.', timeout: 'Request took too long. Please try again.', permission: 'You do not have permission to perform this action.', auth: 'Your session has expired. Please log in again.', notfound: 'The resource you requested could not be found.', unknown: 'An unexpected error occurred. Please try again.' }; const message = messages[category] || messages.unknown; store.notify(`${message} (Error: ${errorId})`, 'error', 5000); } /** * Retry operation with exponential backoff */ async retry(operation, maxRetries = 3, initialDelay = 1000) { for (let i = 0; i < maxRetries; i++) { try { return await operation(); } catch (e) { if (i === maxRetries - 1) { throw e; } const delay = initialDelay * Math.pow(2, i); await new Promise(resolve => setTimeout(resolve, delay)); auditLogger.logWarning('Retrying operation', { attempt: i + 1, maxRetries }); } } } /** * Get crash report */ getCrashReport() { const logs = auditLogger.getLogs(); const errorLogs = logs.filter(l => l.level === 'error'); return { timestamp: new Date().toISOString(), sessionId: auditLogger.sessionId, totalErrors: errorLogs.length, errors: errorLogs.slice(0, 10), recoveryPoints: this.recoveryPoints, lastSnapshot: persistence.getLatestSnapshot(), statistics: auditLogger.getStats() }; } /** * Export crash report */ exportCrashReport() { const report = this.getCrashReport(); return JSON.stringify(report, null, 2); } } // Create and export singleton const errorRecovery = new ErrorRecovery(); export { ErrorRecovery }; export default errorRecovery;