polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
673 lines • 25 kB
JavaScript
;
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