314 lines
7.6 KiB
JavaScript
314 lines
7.6 KiB
JavaScript
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;
|