Skip to main content
Glama
ErrorRecovery.ts10.1 kB
/** * ErrorRecovery - Provides graceful error handling and recovery mechanisms * Ensures the MCP server can recover from browser crashes, network issues, and pattern errors */ import { Logger } from './Logger.js'; export interface RecoveryStrategy { maxRetries: number; retryDelay: number; exponentialBackoff: boolean; fallbackAction?: () => Promise<any>; } export class ErrorRecovery { private logger: Logger; private errorHistory: Map<string, number[]> = new Map(); private readonly ERROR_WINDOW = 60000; // 1 minute window for error tracking constructor() { this.logger = new Logger(); } /** * Executes an operation with automatic retry logic * @param operation - Async function to execute * @param operationName - Name for logging * @param strategy - Recovery strategy configuration * @returns Result of the operation */ async executeWithRetry<T>( operation: () => Promise<T>, operationName: string, strategy: RecoveryStrategy = { maxRetries: 3, retryDelay: 1000, exponentialBackoff: true } ): Promise<T> { let lastError: Error | null = null; for (let attempt = 0; attempt <= strategy.maxRetries; attempt++) { try { this.logger.debug(`Executing ${operationName} (attempt ${attempt + 1}/${strategy.maxRetries + 1})`); const result = await operation(); // Success - clear error history for this operation this.clearErrorHistory(operationName); return result; } catch (error: any) { lastError = error; this.recordError(operationName); this.logger.warn( `${operationName} failed (attempt ${attempt + 1}/${strategy.maxRetries + 1})`, { error: error.message } ); // Don't retry on last attempt if (attempt < strategy.maxRetries) { const delay = strategy.exponentialBackoff ? strategy.retryDelay * Math.pow(2, attempt) : strategy.retryDelay; this.logger.debug(`Waiting ${delay}ms before retry`); await this.sleep(delay); } } } // All retries failed this.logger.error(`${operationName} failed after ${strategy.maxRetries + 1} attempts`); // Try fallback if available if (strategy.fallbackAction) { this.logger.info(`Executing fallback for ${operationName}`); try { return await strategy.fallbackAction(); } catch (fallbackError: any) { this.logger.error(`Fallback failed for ${operationName}`, fallbackError); } } throw new Error( `${operationName} failed after ${strategy.maxRetries + 1} attempts: ${lastError?.message}` ); } /** * Wraps an operation with timeout protection * @param operation - Async function to execute * @param timeout - Timeout in milliseconds * @param operationName - Name for error messages * @returns Result of the operation */ async executeWithTimeout<T>( operation: () => Promise<T>, timeout: number, operationName: string ): Promise<T> { return Promise.race([ operation(), this.createTimeout<T>(timeout, operationName) ]); } /** * Combines retry logic with timeout protection * @param operation - Async function to execute * @param operationName - Name for logging * @param timeout - Timeout per attempt in milliseconds * @param strategy - Recovery strategy * @returns Result of the operation */ async executeWithRetryAndTimeout<T>( operation: () => Promise<T>, operationName: string, timeout: number, strategy?: RecoveryStrategy ): Promise<T> { return this.executeWithRetry( () => this.executeWithTimeout(operation, timeout, operationName), operationName, strategy ); } /** * Gracefully handles browser initialization errors * @param initFunction - Browser initialization function * @returns Initialization result */ async handleBrowserInit(initFunction: () => Promise<string>): Promise<string> { return this.executeWithRetryAndTimeout( initFunction, 'Browser Initialization', 30000, // 30 second timeout { maxRetries: 2, retryDelay: 2000, exponentialBackoff: true, fallbackAction: async () => { this.logger.warn('Browser initialization failed, suggesting headless mode'); return 'Browser initialization failed. Try setting headless:true in config.json'; } } ); } /** * Handles pattern write errors with validation * @param writeFunction - Pattern write function * @param pattern - Pattern to write * @returns Write result */ async handlePatternWrite( writeFunction: (pattern: string) => Promise<string>, pattern: string ): Promise<string> { return this.executeWithRetry( () => writeFunction(pattern), 'Pattern Write', { maxRetries: 2, retryDelay: 500, exponentialBackoff: false, fallbackAction: async () => { // Try writing a simplified version const simplified = this.simplifyPattern(pattern); this.logger.info('Attempting to write simplified pattern'); return writeFunction(simplified); } } ); } /** * Checks if an operation is experiencing repeated failures * @param operationName - Name of operation to check * @param threshold - Number of errors to consider "frequent" * @returns True if operation is failing frequently */ isFrequentlyFailing(operationName: string, threshold: number = 3): boolean { const errors = this.errorHistory.get(operationName) || []; const now = Date.now(); // Count errors within the time window const recentErrors = errors.filter(timestamp => now - timestamp < this.ERROR_WINDOW); return recentErrors.length >= threshold; } /** * Gets error statistics for monitoring * @returns Error statistics by operation */ getErrorStats(): Record<string, { count: number; lastError: Date | null }> { const stats: Record<string, { count: number; lastError: Date | null }> = {}; const now = Date.now(); this.errorHistory.forEach((timestamps, operation) => { const recentErrors = timestamps.filter(ts => now - ts < this.ERROR_WINDOW); stats[operation] = { count: recentErrors.length, lastError: recentErrors.length > 0 ? new Date(Math.max(...recentErrors)) : null }; }); return stats; } /** * Clears error history for a specific operation * @param operationName - Operation to clear */ clearErrorHistory(operationName: string): void { this.errorHistory.delete(operationName); } /** * Clears all error history */ clearAllErrorHistory(): void { this.errorHistory.clear(); } /** * Records an error occurrence * @param operationName - Name of the operation */ private recordError(operationName: string): void { const errors = this.errorHistory.get(operationName) || []; errors.push(Date.now()); // Keep only recent errors const now = Date.now(); const recentErrors = errors.filter(ts => now - ts < this.ERROR_WINDOW); this.errorHistory.set(operationName, recentErrors); } /** * Creates a timeout promise * @param ms - Timeout in milliseconds * @param operationName - Name for error message * @returns Promise that rejects after timeout */ private createTimeout<T>(ms: number, operationName: string): Promise<T> { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`${operationName} timed out after ${ms}ms`)); }, ms); }); } /** * Sleep utility * @param ms - Milliseconds to sleep * @returns Promise that resolves after delay */ private sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Simplifies a pattern by removing complex modifiers * @param pattern - Original pattern * @returns Simplified pattern */ private simplifyPattern(pattern: string): string { // Remove complex modifiers but keep core structure let simplified = pattern; // Remove effect chains but keep basic sound/note calls simplified = simplified.replace(/\.(delay|reverb|room|lpf|hpf|bpf)\([^)]*\)/g, ''); // Remove complex transformations simplified = simplified.replace(/\.(jux|iter|chop|striate|scramble)\([^)]*\)/g, ''); // Remove conditional modifications simplified = simplified.replace(/\.(sometimes|often|rarely|every)\([^)]*\)/g, ''); this.logger.debug('Pattern simplified', { original: pattern.substring(0, 50) + '...', simplified: simplified.substring(0, 50) + '...' }); return simplified; } /** * Handles network-related errors * @param operation - Operation that may fail due to network * @param operationName - Name for logging * @returns Operation result */ async handleNetworkOperation<T>( operation: () => Promise<T>, operationName: string ): Promise<T> { return this.executeWithRetry( operation, operationName, { maxRetries: 5, retryDelay: 2000, exponentialBackoff: true, fallbackAction: async () => { throw new Error(`Network operation ${operationName} failed - check internet connection`); } } ); } /** * Creates a circuit breaker for operations that fail frequently * @param operation - Operation to protect * @param operationName - Name for tracking * @param threshold - Number of failures before circuit opens * @returns Protected operation */ createCircuitBreaker<T>( operation: () => Promise<T>, operationName: string, threshold: number = 5 ): () => Promise<T> { return async () => { if (this.isFrequentlyFailing(operationName, threshold)) { throw new Error( `Circuit breaker open for ${operationName} - operation disabled due to repeated failures` ); } return operation(); }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/williamzujkowski/strudel-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server