UNPKG

polyv-live-cli

Version:

CLI tool for managing PolyV live streaming services.

673 lines 25 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ErrorRecoveryManager = void 0; const events_1 = require("events"); class ErrorRecoveryManager extends events_1.EventEmitter { constructor(config = {}) { super(); this.errorHistory = []; this.errorPatterns = new Map(); this.recoveryStrategies = new Map(); this.errorIdCounter = 0; this.patternIdCounter = 0; this.isRunning = false; this.criticalErrorCount = 0; this.fallbackModeActive = false; this.config = { enableAutoRecovery: true, maxRetryAttempts: 3, baseRetryDelay: 1000, retryDelayMultiplier: 2, maxRetryDelay: 30000, errorHistorySize: 1000, criticalErrorThreshold: 5, recoveryTimeout: 30000, enableErrorLogging: true, enableUserNotifications: true, fallbackModeTimeout: 300000, ...config, }; this.initializeRecoveryStrategies(); this.initializeErrorPatterns(); this.setupEventHandlers(); } start() { if (this.isRunning) { return; } this.isRunning = true; this.criticalErrorCount = 0; this.fallbackModeActive = false; this.emit('recoveryManagerStarted', { timestamp: Date.now(), config: this.config, }); } stop() { if (!this.isRunning) { return; } this.isRunning = false; if (this.fallbackModeTimer) { clearTimeout(this.fallbackModeTimer); delete this.fallbackModeTimer; } const finalStats = this.getStatistics(); this.emit('recoveryManagerStopped', { timestamp: Date.now(), finalStats, }); } async reportError(error, context) { const errorRecord = this.createErrorRecord(error, context); const existingError = this.findExistingError(errorRecord); if (existingError) { existingError.occurrenceCount++; existingError.lastOccurrence = Date.now(); this.updateErrorPattern(existingError); } else { this.errorHistory.push(errorRecord); this.learnErrorPattern(errorRecord); } if (this.errorHistory.length > this.config.errorHistorySize) { this.errorHistory = this.errorHistory.slice(-this.config.errorHistorySize); } if (this.config.enableErrorLogging) { this.logError(errorRecord); } this.emit('errorReported', { error: errorRecord, timestamp: Date.now(), }); if (this.config.enableAutoRecovery && this.isRunning) { await this.attemptRecovery(errorRecord); } if (errorRecord.severity === 'critical') { this.criticalErrorCount++; if (this.criticalErrorCount >= this.config.criticalErrorThreshold) { this.activateFallbackMode(); } } } async attemptRecovery(errorRecord) { if (errorRecord.recoveryAttempts >= errorRecord.maxRetries) { return { success: false, strategy: 'escalate', recoveryTime: 0, error: errorRecord, details: 'Maximum retry attempts exceeded', }; } const startTime = Date.now(); errorRecord.recoveryAttempts++; try { const strategy = this.determineRecoveryStrategy(errorRecord); const recoveryFunction = this.recoveryStrategies.get(strategy); if (!recoveryFunction) { throw new Error(`Unknown recovery strategy: ${strategy}`); } const result = await Promise.race([ recoveryFunction(errorRecord), this.createTimeoutPromise(this.config.recoveryTimeout), ]); errorRecord.recoveryStrategy = strategy; errorRecord.recoveryTime = Date.now() - startTime; if (result.success) { errorRecord.recovered = true; this.emit('errorRecovered', { error: errorRecord, result, timestamp: Date.now(), }); } else { this.emit('recoveryFailed', { error: errorRecord, result, timestamp: Date.now(), }); } return result; } catch (recoveryError) { const result = { success: false, strategy: 'escalate', recoveryTime: Date.now() - startTime, error: errorRecord, details: 'Recovery attempt failed', newError: recoveryError instanceof Error ? recoveryError : new Error('Unknown recovery error'), }; this.emit('recoveryFailed', { error: errorRecord, result, timestamp: Date.now(), }); return result; } } getStatistics() { const totalErrors = this.errorHistory.length; const errorsByType = { network: 0, api: 0, render: 0, memory: 0, configuration: 0, validation: 0, timeout: 0, permission: 0, unknown: 0, }; const errorsBySeverity = { low: 0, medium: 0, high: 0, critical: 0, }; const errorsByComponent = {}; let totalRecoveryTime = 0; let recoveredErrors = 0; let recentErrors = 0; const oneHourAgo = Date.now() - 3600000; for (const error of this.errorHistory) { errorsByType[error.type]++; errorsBySeverity[error.severity]++; const component = error.context.component; errorsByComponent[component] = (errorsByComponent[component] || 0) + 1; if (error.recovered && error.recoveryTime) { totalRecoveryTime += error.recoveryTime; recoveredErrors++; } if (error.lastOccurrence > oneHourAgo) { recentErrors++; } } const recoverySuccessRate = totalErrors > 0 ? recoveredErrors / totalErrors : 0; const averageRecoveryTime = recoveredErrors > 0 ? totalRecoveryTime / recoveredErrors : 0; return { totalErrors, errorsByType, errorsBySeverity, errorsByComponent, recoverySuccessRate, averageRecoveryTime, commonPatterns: Array.from(this.errorPatterns.values()) .sort((a, b) => b.occurrences - a.occurrences) .slice(0, 10), trends: { errorRate: recentErrors / Math.max(1, totalErrors), recoveryRate: recoverySuccessRate, criticalErrors: errorsBySeverity.critical, recentErrors, }, }; } getErrorHistory(timeRange) { if (!timeRange) { return [...this.errorHistory]; } return this.errorHistory.filter(error => error.lastOccurrence >= timeRange.start && error.lastOccurrence <= timeRange.end); } getUserErrorMessage(errorRecord) { const severity = this.mapSeverityToUser(errorRecord.severity); const recoverable = this.isErrorRecoverable(errorRecord); const recoveryStatus = this.getRecoveryStatus(errorRecord); return { id: errorRecord.id, title: this.generateUserFriendlyTitle(errorRecord), description: this.generateUserFriendlyDescription(errorRecord), suggestedActions: this.generateUserActions(errorRecord), severity, recoverable, recoveryStatus, }; } clearErrorHistory() { const clearedCount = this.errorHistory.length; this.errorHistory = []; this.errorPatterns.clear(); this.criticalErrorCount = 0; this.emit('errorHistoryCleared', { clearedCount, timestamp: Date.now(), }); } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.emit('configUpdated', { config: this.config, timestamp: Date.now(), }); } registerRecoveryStrategy(name, strategy) { this.recoveryStrategies.set(name, strategy); this.emit('recoveryStrategyRegistered', { name, timestamp: Date.now(), }); } activateFallbackMode() { if (this.fallbackModeActive) { return; } this.fallbackModeActive = true; this.emit('fallbackModeActivated', { reason: 'Critical error threshold exceeded', timestamp: Date.now(), }); this.fallbackModeTimer = setTimeout(() => { this.deactivateFallbackMode(); }, this.config.fallbackModeTimeout); } deactivateFallbackMode() { if (!this.fallbackModeActive) { return; } this.fallbackModeActive = false; this.criticalErrorCount = 0; if (this.fallbackModeTimer) { clearTimeout(this.fallbackModeTimer); delete this.fallbackModeTimer; } this.emit('fallbackModeDeactivated', { timestamp: Date.now(), }); } isFallbackModeActive() { return this.fallbackModeActive; } createErrorRecord(error, context) { const errorType = this.classifyError(error); const severity = this.determineSeverity(error, errorType); const maxRetries = this.getMaxRetries(errorType, severity); return { id: `error_${++this.errorIdCounter}`, type: errorType, severity, message: error.message, context: { ...context, timestamp: Date.now(), }, recoveryAttempts: 0, maxRetries, recovered: false, firstOccurrence: Date.now(), lastOccurrence: Date.now(), occurrenceCount: 1, ...(error.stack && { stack: error.stack }), }; } classifyError(error) { const message = error.message.toLowerCase(); const name = error.name.toLowerCase(); if (message.includes('network') || message.includes('fetch') || message.includes('connection')) { return 'network'; } if (message.includes('api') || message.includes('request') || message.includes('response')) { return 'api'; } if (message.includes('render') || message.includes('display') || message.includes('ui')) { return 'render'; } if (message.includes('memory') || name.includes('memory')) { return 'memory'; } if (message.includes('config') || message.includes('setting')) { return 'configuration'; } if (message.includes('validation') || message.includes('invalid')) { return 'validation'; } if (message.includes('timeout') || name.includes('timeout')) { return 'timeout'; } if (message.includes('permission') || message.includes('unauthorized')) { return 'permission'; } return 'unknown'; } determineSeverity(error, errorType) { const message = error.message.toLowerCase(); if (message.includes('critical') || message.includes('fatal') || errorType === 'memory') { return 'critical'; } if (message.includes('error') || errorType === 'api' || errorType === 'network') { return 'high'; } if (message.includes('warning') || errorType === 'render' || errorType === 'timeout') { return 'medium'; } return 'low'; } getMaxRetries(errorType, severity) { const baseRetries = this.config.maxRetryAttempts; if (severity === 'critical') { return Math.max(baseRetries + 2, 5); } if (severity === 'high') { return baseRetries + 1; } if (errorType === 'network' || errorType === 'api') { return baseRetries + 1; } return baseRetries; } findExistingError(newError) { const recentThreshold = Date.now() - 300000; return this.errorHistory.find(existing => existing.type === newError.type && existing.context.component === newError.context.component && existing.context.operation === newError.context.operation && existing.message === newError.message && existing.lastOccurrence > recentThreshold) || null; } learnErrorPattern(errorRecord) { const patternId = `${errorRecord.type}_${errorRecord.context.component}`; let pattern = this.errorPatterns.get(patternId); if (!pattern) { pattern = { id: `pattern_${++this.patternIdCounter}`, description: `${errorRecord.type} errors in ${errorRecord.context.component}`, errorType: errorRecord.type, componentPattern: errorRecord.context.component, messagePattern: new RegExp(errorRecord.message.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'), recoveryStrategy: this.determineRecoveryStrategy(errorRecord), confidence: 0.1, occurrences: 0, lastSeen: Date.now(), }; this.errorPatterns.set(patternId, pattern); } pattern.occurrences++; pattern.lastSeen = Date.now(); pattern.confidence = Math.min(pattern.confidence + 0.1, 1.0); } updateErrorPattern(errorRecord) { const patternId = `${errorRecord.type}_${errorRecord.context.component}`; const pattern = this.errorPatterns.get(patternId); if (pattern) { pattern.occurrences++; pattern.lastSeen = Date.now(); pattern.confidence = Math.min(pattern.confidence + 0.05, 1.0); } } determineRecoveryStrategy(errorRecord) { const patternId = `${errorRecord.type}_${errorRecord.context.component}`; const pattern = this.errorPatterns.get(patternId); if (pattern && pattern.confidence > 0.7) { return pattern.recoveryStrategy; } switch (errorRecord.type) { case 'network': case 'api': case 'timeout': return 'retry'; case 'render': return 'reset'; case 'memory': return 'fallback'; case 'configuration': return 'reset'; case 'validation': return 'ignore'; case 'permission': return 'escalate'; default: return 'retry'; } } initializeRecoveryStrategies() { this.recoveryStrategies.set('retry', async (error) => { const delay = Math.min(this.config.baseRetryDelay * Math.pow(this.config.retryDelayMultiplier, error.recoveryAttempts - 1), this.config.maxRetryDelay); await new Promise(resolve => setTimeout(resolve, delay)); return { success: true, strategy: 'retry', recoveryTime: delay, error, details: `Retry after ${delay}ms delay`, }; }); this.recoveryStrategies.set('fallback', async (error) => { this.activateFallbackMode(); return { success: true, strategy: 'fallback', recoveryTime: 0, error, details: 'Activated fallback mode', }; }); this.recoveryStrategies.set('reset', async (error) => { this.emit('resetRequested', { component: error.context.component, operation: error.context.operation, timestamp: Date.now(), }); return { success: true, strategy: 'reset', recoveryTime: 0, error, details: 'Reset requested', }; }); this.recoveryStrategies.set('ignore', async (error) => { return { success: true, strategy: 'ignore', recoveryTime: 0, error, details: 'Error ignored', }; }); this.recoveryStrategies.set('escalate', async (error) => { this.emit('errorEscalated', { error, timestamp: Date.now(), }); return { success: false, strategy: 'escalate', recoveryTime: 0, error, details: 'Error escalated', }; }); this.recoveryStrategies.set('restart', async (error) => { this.emit('restartRequested', { component: error.context.component, reason: error.message, timestamp: Date.now(), }); return { success: true, strategy: 'restart', recoveryTime: 0, error, details: 'Restart requested', }; }); } initializeErrorPatterns() { this.errorPatterns.set('network_common', { id: 'network_common', description: 'Common network errors', errorType: 'network', componentPattern: '*', messagePattern: /(network|connection|fetch|timeout)/i, recoveryStrategy: 'retry', confidence: 0.8, occurrences: 0, lastSeen: Date.now(), }); this.errorPatterns.set('api_common', { id: 'api_common', description: 'Common API errors', errorType: 'api', componentPattern: '*', messagePattern: /(api|request|response|http)/i, recoveryStrategy: 'retry', confidence: 0.8, occurrences: 0, lastSeen: Date.now(), }); } createTimeoutPromise(timeout) { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Recovery timeout after ${timeout}ms`)); }, timeout); }); } logError(errorRecord) { const logMessage = `[${errorRecord.severity.toUpperCase()}] ${errorRecord.type} error in ${errorRecord.context.component}: ${errorRecord.message}`; if (errorRecord.severity === 'critical') { console.error(logMessage); } else if (errorRecord.severity === 'high') { console.warn(logMessage); } else { console.log(logMessage); } } mapSeverityToUser(severity) { switch (severity) { case 'low': return 'info'; case 'medium': return 'warning'; case 'high': case 'critical': return 'error'; default: return 'info'; } } isErrorRecoverable(errorRecord) { return errorRecord.recoveryAttempts < errorRecord.maxRetries; } getRecoveryStatus(errorRecord) { if (errorRecord.recovered) { return 'completed'; } if (errorRecord.recoveryAttempts >= errorRecord.maxRetries) { return 'failed'; } if (errorRecord.recoveryAttempts > 0) { return 'in-progress'; } return 'pending'; } generateUserFriendlyTitle(errorRecord) { switch (errorRecord.type) { case 'network': return 'Network Connection Issue'; case 'api': return 'Service Communication Error'; case 'render': return 'Display Update Problem'; case 'memory': return 'Memory Usage Issue'; case 'configuration': return 'Configuration Problem'; case 'validation': return 'Data Validation Error'; case 'timeout': return 'Operation Timeout'; case 'permission': return 'Permission Denied'; default: return 'Unexpected Error'; } } generateUserFriendlyDescription(errorRecord) { const component = errorRecord.context.component; const operation = errorRecord.context.operation; switch (errorRecord.type) { case 'network': return `Unable to connect to the monitoring service. This may be due to network connectivity issues or service unavailability.`; case 'api': return `Failed to communicate with the monitoring API while ${operation} in ${component}. The service may be temporarily unavailable.`; case 'render': return `There was a problem updating the display for ${component}. Some information may not be showing correctly.`; case 'memory': return `The application is using more memory than expected. Performance may be affected.`; case 'configuration': return `There's an issue with the configuration settings for ${component}. Please check your settings.`; case 'validation': return `The data received from the service doesn't match expected format. This may be temporary.`; case 'timeout': return `The operation ${operation} in ${component} took too long to complete and was cancelled.`; case 'permission': return `Access denied while trying to ${operation} in ${component}. Please check your permissions.`; default: return `An unexpected error occurred in ${component} while ${operation}. The system will attempt to recover automatically.`; } } generateUserActions(errorRecord) { const actions = []; switch (errorRecord.type) { case 'network': actions.push('Check your internet connection'); actions.push('Try refreshing the application'); actions.push('Wait a moment and try again'); break; case 'api': actions.push('Wait a moment and try again'); actions.push('Check service status'); actions.push('Verify your API credentials'); break; case 'render': actions.push('Refresh the display'); actions.push('Restart the application if problems persist'); break; case 'memory': actions.push('Close unnecessary applications'); actions.push('Restart the monitoring tool'); break; case 'configuration': actions.push('Check your configuration settings'); actions.push('Reset to default settings if needed'); break; case 'validation': actions.push('Wait for the next update'); actions.push('Contact support if problems persist'); break; case 'timeout': actions.push('Try the operation again'); actions.push('Check your network connection'); break; case 'permission': actions.push('Contact your administrator'); actions.push('Check your access permissions'); break; default: actions.push('Wait for automatic recovery'); actions.push('Restart the application if problems persist'); break; } return actions; } setupEventHandlers() { process.on('uncaughtException', (error) => { this.reportError(error, { component: 'process', operation: 'global', }); }); process.on('unhandledRejection', (reason) => { const error = reason instanceof Error ? reason : new Error(String(reason)); this.reportError(error, { component: 'process', operation: 'promise', }); }); } } exports.ErrorRecoveryManager = ErrorRecoveryManager; //# sourceMappingURL=error-recovery-manager.js.map