Start on step 17 next
This commit is contained in:
313
src/tui/services/ErrorHandlingService.js
Normal file
313
src/tui/services/ErrorHandlingService.js
Normal file
@@ -0,0 +1,313 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user