const { enhanceError, isRetryableError, getRetryDelay, createStandardError, logError, } = require("../utils/errorHandler"); /** * ErrorHandlingService - Centralized error handling for TUI operations * Requirements: 4.5, 16.1 */ class ErrorHandlingService { constructor() { this.errorHistory = []; this.maxHistorySize = 100; } /** * Execute an operation with comprehensive error handling and retry logic * @param {Function} operation - Async operation to execute * @param {Object} options - Error handling options * @returns {Promise} Operation result */ async executeWithRetry(operation, options = {}) { const { maxRetries = 3, retryDelay = null, context = {}, onRetry = null, onError = null, retryableCheck = null, } = options; let lastError = null; let attempt = 0; while (attempt < maxRetries) { attempt++; try { const result = await operation(); // Log successful retry if this wasn't the first attempt if (attempt > 1) { console.info( `Operation succeeded on attempt ${attempt}/${maxRetries}` ); } return result; } catch (error) { lastError = error; // Enhance the error with context const enhancedError = enhanceError(error, { ...context, attempt, maxRetries, }); // Log the error logError(enhancedError, context); // Add to error history this.addToHistory(enhancedError); // Call error callback if provided if (onError) { onError(enhancedError, attempt); } // Check if we should retry const shouldRetry = retryableCheck ? retryableCheck(enhancedError) : isRetryableError(enhancedError); if (attempt >= maxRetries || !shouldRetry) { break; } // Calculate delay const delay = retryDelay || getRetryDelay(attempt, enhancedError.category); console.warn( `Operation failed (attempt ${attempt}/${maxRetries}), retrying in ${delay}ms:`, error.message ); // Call retry callback if provided if (onRetry) { onRetry(attempt, delay, enhancedError); } // Wait before retrying await this.delay(delay); } } // All retries failed, throw the last error throw enhanceError(lastError, { ...context, finalAttempt: true, totalAttempts: attempt, message: `Operation failed after ${attempt} attempts`, }); } /** * Handle file operation errors with specific retry logic * @param {Function} fileOperation - File operation to execute * @param {Object} options - Options * @returns {Promise} Operation result */ async handleFileOperation(fileOperation, options = {}) { return this.executeWithRetry(fileOperation, { maxRetries: 3, context: { operation: "file_operation", ...options.context, }, retryableCheck: (error) => { // File operations are retryable for certain errors const retryableCodes = ["EBUSY", "EMFILE", "ENFILE", "EAGAIN"]; return ( retryableCodes.includes(error.code) || error.message.includes("locked") || error.message.includes("busy") ); }, ...options, }); } /** * Handle API operation errors with exponential backoff * @param {Function} apiOperation - API operation to execute * @param {Object} options - Options * @returns {Promise} Operation result */ async handleApiOperation(apiOperation, options = {}) { return this.executeWithRetry(apiOperation, { maxRetries: 5, context: { operation: "api_operation", ...options.context, }, retryableCheck: (error) => { // API operations are retryable for network and rate limit errors return ( isRetryableError(error) || error.message.includes("rate limit") || error.message.includes("timeout") ); }, ...options, }); } /** * Handle validation errors with user-friendly messages * @param {Function} validationOperation - Operation that might have validation errors * @param {Object} options - Options * @returns {Promise} Operation result */ async handleValidation(validationOperation, options = {}) { try { return await validationOperation(); } catch (error) { // Don't retry validation errors, but enhance them const enhancedError = enhanceError(error, { operation: "validation", ...options.context, troubleshooting: [ "Check that all required fields are filled", "Verify the data format is correct", "Ensure values are within acceptable ranges", ...(options.troubleshooting || []), ], }); this.addToHistory(enhancedError); throw enhancedError; } } /** * Create a graceful fallback for missing files * @param {Function} fileReader - Function to read file * @param {*} fallbackValue - Value to return if file doesn't exist * @param {Object} options - Options * @returns {Promise} File content or fallback value */ async gracefulFileRead(fileReader, fallbackValue, options = {}) { try { return await fileReader(); } catch (error) { if (error.code === "ENOENT") { console.info( `File not found, using fallback value: ${ options.filename || "unknown file" }` ); return fallbackValue; } // For other errors, use normal error handling throw enhanceError(error, { operation: "graceful_file_read", filename: options.filename, ...options.context, }); } } /** * Add error to history for debugging * @param {Error} error - Error to add */ addToHistory(error) { this.errorHistory.unshift({ timestamp: new Date().toISOString(), error: { message: error.message, category: error.category, context: error.context, stack: error.stack, }, }); // Keep history size manageable if (this.errorHistory.length > this.maxHistorySize) { this.errorHistory = this.errorHistory.slice(0, this.maxHistorySize); } } /** * Get recent error history * @param {number} limit - Number of recent errors to return * @returns {Array} Recent errors */ getErrorHistory(limit = 10) { return this.errorHistory.slice(0, limit); } /** * Clear error history */ clearHistory() { this.errorHistory = []; } /** * Get error statistics * @returns {Object} Error statistics */ getErrorStats() { const categories = {}; const operations = {}; this.errorHistory.forEach((entry) => { const category = entry.error.category || "unknown"; const operation = entry.error.context?.operation || "unknown"; categories[category] = (categories[category] || 0) + 1; operations[operation] = (operations[operation] || 0) + 1; }); return { totalErrors: this.errorHistory.length, categories, operations, mostRecentError: this.errorHistory[0] || null, }; } /** * Create a user-friendly error message for display * @param {Error} error - Error to format * @returns {string} User-friendly message */ formatUserMessage(error) { if (!error) return "An unknown error occurred"; const category = error.category || "system"; const baseMessage = error.message; const categoryMessages = { network: "Connection problem - please check your internet connection", api: "Shopify API issue - please verify your store settings", file: "File access problem - please check file permissions", validation: "Input validation error - please check your data", system: "System error - please try again", }; const categoryMessage = categoryMessages[category] || categoryMessages.system; return `${categoryMessage}: ${baseMessage}`; } /** * Delay execution * @param {number} ms - Milliseconds to delay * @returns {Promise} Promise that resolves after delay */ delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } } module.exports = ErrorHandlingService;