Start on step 17 next

This commit is contained in:
2025-08-15 15:39:28 -05:00
parent 62f6d6f279
commit 7be928a5be
18 changed files with 3163 additions and 1347 deletions

View 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;