UNPKG

@podx/core

Version:

🚀 Core utilities and shared functionality for PODx - Advanced Twitter/X scraping and crypto analysis toolkit

353 lines (304 loc) • 10.9 kB
/** * Enhanced error recovery strategies and automated problem resolution */ import { logger } from '../logger/index.js'; import type { LogContext } from '../logger/index.js'; import type { CategorizedError, ErrorCategory } from '../types/common.js'; import { errorHandling } from '../error-handling/index.js'; // Recovery strategy interface export interface RecoveryStrategy<T = unknown> { readonly name: string; readonly canRecover: (error: CategorizedError) => boolean; readonly recover: (error: CategorizedError, context?: LogContext) => Promise<errorHandling.Result<T, CategorizedError>>; readonly priority: number; // Higher number = higher priority readonly timeout: number; // Maximum time to spend on recovery } // Recovery context for strategies export interface RecoveryContext extends LogContext { readonly attempt: number; readonly maxAttempts: number; readonly previousErrors: CategorizedError[]; readonly recoveryHistory: RecoveryAttempt[]; } export interface RecoveryAttempt { readonly strategy: string; readonly success: boolean; readonly duration: number; readonly timestamp: number; readonly error?: string; } // Enhanced recovery manager export class EnhancedRecoveryManager { private static readonly strategies = new Map<string, RecoveryStrategy>(); private static readonly recoveryHistory: RecoveryAttempt[] = []; private static readonly maxHistorySize = 1000; /** * Register a recovery strategy */ static registerStrategy<T>(strategy: RecoveryStrategy<T>): void { this.strategies.set(strategy.name, strategy); logger.debug('Recovery strategy registered', { strategyName: strategy.name, priority: strategy.priority, timeout: strategy.timeout }); } /** * Attempt to recover from an error with multiple strategies */ static async attemptRecovery<T>( error: CategorizedError, context?: LogContext ): Promise<errorHandling.Result<T, CategorizedError>> { const applicableStrategies = this.getApplicableStrategies(error); if (applicableStrategies.length === 0) { logger.debug('No recovery strategies available', { errorCategory: error.category, errorMessage: error.message, ...context }); return errorHandling.result.err(error); } const recoveryContext: RecoveryContext = { ...context, attempt: 1, maxAttempts: applicableStrategies.length, previousErrors: [error], recoveryHistory: [] }; logger.info('Attempting error recovery', { errorCategory: error.category, applicableStrategies: applicableStrategies.map(s => s.name), ...context }); // Try strategies in priority order for (const strategy of applicableStrategies) { const startTime = performance.now(); try { const result = await Promise.race([ strategy.recover(error, recoveryContext), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Recovery timeout')), strategy.timeout) ) ]); const duration = performance.now() - startTime; const attempt: RecoveryAttempt = { strategy: strategy.name, success: result.success, duration, timestamp: Date.now() }; this.addRecoveryAttempt(attempt); if (result.success) { logger.info('Error recovery successful', { strategyName: strategy.name, duration, errorCategory: error.category, ...context }); return result; } else { logger.warn('Recovery strategy failed', { strategyName: strategy.name, duration, error: result.error.message, ...context }); } } catch (recoveryError) { const duration = performance.now() - startTime; const attempt: RecoveryAttempt = { strategy: strategy.name, success: false, duration, timestamp: Date.now(), error: (recoveryError as Error).message }; this.addRecoveryAttempt(attempt); logger.warn('Recovery strategy error', { strategyName: strategy.name, duration, error: (recoveryError as Error).message, ...context }); } } logger.error('All recovery strategies failed', { errorCategory: error.category, attemptedStrategies: applicableStrategies.map(s => s.name), ...context }); return errorHandling.result.err(error); } /** * Get applicable strategies for an error, sorted by priority */ private static getApplicableStrategies(error: CategorizedError): RecoveryStrategy[] { return Array.from(this.strategies.values()) .filter(strategy => strategy.canRecover(error)) .sort((a, b) => b.priority - a.priority); } /** * Add recovery attempt to history */ private static addRecoveryAttempt(attempt: RecoveryAttempt): void { this.recoveryHistory.push(attempt); // Keep only recent attempts if (this.recoveryHistory.length > this.maxHistorySize) { this.recoveryHistory.shift(); } } /** * Get recovery statistics */ static getRecoveryStats(): { totalAttempts: number; successRate: number; averageDuration: number; strategyStats: Record<string, { attempts: number; successes: number; averageDuration: number; }>; } { const totalAttempts = this.recoveryHistory.length; const successfulAttempts = this.recoveryHistory.filter(a => a.success).length; const successRate = totalAttempts > 0 ? successfulAttempts / totalAttempts : 0; const totalDuration = this.recoveryHistory.reduce((sum, a) => sum + a.duration, 0); const averageDuration = totalAttempts > 0 ? totalDuration / totalAttempts : 0; // Calculate strategy statistics const strategyStats: Record<string, { attempts: number; successes: number; averageDuration: number; }> = {}; for (const attempt of this.recoveryHistory) { if (!strategyStats[attempt.strategy]) { strategyStats[attempt.strategy] = { attempts: 0, successes: 0, averageDuration: 0 }; } const stats = strategyStats[attempt.strategy]; stats.attempts++; if (attempt.success) { stats.successes++; } stats.averageDuration = (stats.averageDuration * (stats.attempts - 1) + attempt.duration) / stats.attempts; } return { totalAttempts, successRate, averageDuration, strategyStats }; } /** * Clear recovery history */ static clearHistory(): void { this.recoveryHistory.length = 0; } } // Built-in recovery strategies export const createNetworkRecoveryStrategy = (): RecoveryStrategy => ({ name: 'network-retry', priority: 10, timeout: 5000, canRecover: (error) => error.category === 'NETWORK' && error.retryable, recover: async (error, context) => { logger.info('Attempting network retry recovery', { ...context }); // Wait for a short period before retrying await new Promise(resolve => setTimeout(resolve, 1000)); // In a real implementation, you would retry the original operation // For now, we'll simulate a successful recovery return errorHandling.result.ok('network-recovered'); } }); export const createRateLimitRecoveryStrategy = (): RecoveryStrategy => ({ name: 'rate-limit-wait', priority: 20, timeout: 30000, canRecover: (error) => error.category === 'RATE_LIMIT' && error.retryAfter !== undefined, recover: async (error, context) => { if (!error.retryAfter) { return errorHandling.result.err(error); } logger.info('Waiting for rate limit reset', { retryAfter: error.retryAfter, ...context }); await new Promise(resolve => setTimeout(resolve, error.retryAfter!)); return errorHandling.result.ok('rate-limit-reset'); } }); export const createDatabaseRecoveryStrategy = (): RecoveryStrategy => ({ name: 'database-reconnect', priority: 15, timeout: 10000, canRecover: (error) => error.category === 'DATABASE' && error.retryable, recover: async (error, context) => { logger.info('Attempting database reconnection', { ...context }); // Simulate database reconnection await new Promise(resolve => setTimeout(resolve, 2000)); // In a real implementation, you would attempt to reconnect to the database return errorHandling.result.ok('database-reconnected'); } }); export const createConfigurationRecoveryStrategy = (): RecoveryStrategy => ({ name: 'configuration-reload', priority: 5, timeout: 5000, canRecover: (error) => error.category === 'CONFIGURATION', recover: async (error, context) => { logger.info('Attempting configuration reload', { ...context }); // Simulate configuration reload await new Promise(resolve => setTimeout(resolve, 1000)); // In a real implementation, you would reload configuration return errorHandling.result.ok('configuration-reloaded'); } }); export const createFallbackRecoveryStrategy = (): RecoveryStrategy => ({ name: 'fallback-operation', priority: 1, timeout: 15000, canRecover: (error) => error.category === 'EXTERNAL_SERVICE' || error.category === 'BUSINESS_LOGIC', recover: async (error, context) => { logger.info('Attempting fallback operation', { ...context }); // Simulate fallback operation await new Promise(resolve => setTimeout(resolve, 3000)); // In a real implementation, you would perform a fallback operation return errorHandling.result.ok('fallback-completed'); } }); // Initialize built-in recovery strategies export const initializeRecoveryStrategies = (): void => { EnhancedRecoveryManager.registerStrategy(createNetworkRecoveryStrategy()); EnhancedRecoveryManager.registerStrategy(createRateLimitRecoveryStrategy()); EnhancedRecoveryManager.registerStrategy(createDatabaseRecoveryStrategy()); EnhancedRecoveryManager.registerStrategy(createConfigurationRecoveryStrategy()); EnhancedRecoveryManager.registerStrategy(createFallbackRecoveryStrategy()); logger.info('Recovery strategies initialized', { strategyCount: 5, strategies: [ 'network-retry', 'rate-limit-wait', 'database-reconnect', 'configuration-reload', 'fallback-operation' ] }); }; // Export recovery utilities export const recovery = { manager: EnhancedRecoveryManager, strategies: { network: createNetworkRecoveryStrategy, rateLimit: createRateLimitRecoveryStrategy, database: createDatabaseRecoveryStrategy, configuration: createConfigurationRecoveryStrategy, fallback: createFallbackRecoveryStrategy }, initialize: initializeRecoveryStrategies };