ai-seo
Version:
AI-native JSON-LD schema utility with automated URL-to-Schema generation, intelligent content analysis, caching, rate limiting, performance monitoring, and AI optimization (ChatGPT, Voice). Complete automation & scale features. Zero runtime dependencies.
298 lines (254 loc) • 7.35 kB
JavaScript
/**
* Error Recovery - v1.12.0
* Robust error handling and recovery strategies
*
* Features:
* - Retry mechanisms with exponential backoff
* - Partial result handling
* - Graceful degradation
* - Error classification
* - Recovery strategies
*/
export class ErrorRecovery {
constructor(options = {}) {
this.options = {
maxRetries: options.maxRetries || 3,
initialDelay: options.initialDelay || 1000,
maxDelay: options.maxDelay || 30000,
backoffMultiplier: options.backoffMultiplier || 2,
retryableErrors: options.retryableErrors || ['ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'FETCH_ERROR'],
enabled: options.enabled !== false,
verbose: options.verbose || false
};
this.stats = {
totalAttempts: 0,
successfulRetries: 0,
failedRetries: 0,
partialResults: 0
};
}
/**
* Execute function with retry logic
* @param {Function} fn - Function to execute
* @param {Object} options - Retry options
* @returns {Promise<*>} Function result
*/
async retry(fn, options = {}) {
const maxRetries = options.maxRetries || this.options.maxRetries;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
this.stats.totalAttempts++;
try {
const result = await fn();
if (attempt > 0) {
this.stats.successfulRetries++;
}
return result;
} catch (error) {
lastError = error;
// Check if error is retryable
if (!this._isRetryable(error)) {
throw error;
}
// Last attempt, throw error
if (attempt === maxRetries) {
this.stats.failedRetries++;
throw error;
}
// Calculate backoff delay
const delay = this._calculateBackoff(attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
/**
* Check if error is retryable
* @private
*/
_isRetryable(error) {
if (!this.options.enabled) {
return false;
}
const errorCode = error.code || error.message || '';
return this.options.retryableErrors.some(retryable =>
errorCode.includes(retryable)
);
}
/**
* Calculate exponential backoff delay
* @private
*/
_calculateBackoff(attempt) {
const delay = Math.min(
this.options.initialDelay * Math.pow(this.options.backoffMultiplier, attempt),
this.options.maxDelay
);
// Add jitter (random ±20%)
const jitter = delay * 0.2 * (Math.random() * 2 - 1);
return Math.round(delay + jitter);
}
/**
* Process batch with partial result handling
* @param {Array} items - Items to process
* @param {Function} fn - Processing function
* @param {Object} options - Processing options
* @returns {Promise<Object>} Results with success/failure breakdown
*/
async batchWithPartialResults(items, fn, options = {}) {
const {
continueOnError = true,
collectErrors = true
} = options;
const results = {
successful: [],
failed: [],
total: items.length,
successCount: 0,
failureCount: 0
};
for (let i = 0; i < items.length; i++) {
const item = items[i];
try {
const result = await this.retry(() => fn(item, i));
results.successful.push({ item, result, index: i });
results.successCount++;
} catch (error) {
results.failureCount++;
if (collectErrors) {
results.failed.push({
item,
error: error.message,
index: i
});
}
if (!continueOnError) {
break;
}
}
}
if (results.failureCount > 0) {
this.stats.partialResults++;
}
return results;
}
/**
* Graceful degradation wrapper
* @param {Function} fn - Primary function
* @param {Function} fallback - Fallback function
* @returns {Promise<*>} Result
*/
async withFallback(fn, fallback) {
try {
return await this.retry(fn);
} catch (error) {
/* eslint-disable-next-line no-console */
if (this.options.verbose) {
console.warn(`[ErrorRecovery] Primary function failed, using fallback: ${error.message}`);
}
try {
return await fallback(error);
} catch (fallbackError) {
throw new Error(`Both primary and fallback failed: ${error.message}, ${fallbackError.message}`);
}
}
}
/**
* Circuit breaker pattern
* @param {Function} fn - Function to protect
* @param {Object} options - Circuit breaker options
* @returns {Function} Protected function
*/
createCircuitBreaker(fn, options = {}) {
const {
failureThreshold = 5,
resetTimeout = 60000
} = options;
let state = 'closed'; // closed, open, half-open
let failureCount = 0;
let lastFailureTime = 0;
return async (...args) => {
// Check if circuit should reset
if (state === 'open' && Date.now() - lastFailureTime > resetTimeout) {
state = 'half-open';
failureCount = 0;
}
// Circuit is open, fail fast
if (state === 'open') {
throw new Error('Circuit breaker is open');
}
try {
const result = await fn(...args);
// Success - close circuit
if (state === 'half-open') {
state = 'closed';
}
failureCount = 0;
return result;
} catch (error) {
failureCount++;
lastFailureTime = Date.now();
// Open circuit if threshold reached
if (failureCount >= failureThreshold) {
state = 'open';
}
throw error;
}
};
}
/**
* Get recovery statistics
* @returns {Object} Statistics
*/
getStats() {
return {
...this.stats,
successRate: this.stats.totalAttempts > 0
? ((this.stats.successfulRetries / this.stats.totalAttempts) * 100).toFixed(2) + '%'
: '0%'
};
}
/**
* Reset statistics
*/
resetStats() {
this.stats = {
totalAttempts: 0,
successfulRetries: 0,
failedRetries: 0,
partialResults: 0
};
}
}
// Global recovery instance
let globalRecovery = null;
/**
* Get global error recovery instance
* @param {Object} options - Recovery options
* @returns {ErrorRecovery} Recovery instance
*/
export function getErrorRecovery(options = {}) {
if (!globalRecovery) {
globalRecovery = new ErrorRecovery(options);
}
return globalRecovery;
}
/**
* Reset global recovery
*/
export function resetErrorRecovery() {
if (globalRecovery) {
globalRecovery.resetStats();
}
globalRecovery = null;
}
/**
* Retry helper function
* @param {Function} fn - Function to retry
* @param {Object} options - Retry options
* @returns {Promise<*>} Result
*/
export async function retry(fn, options = {}) {
const recovery = getErrorRecovery();
return recovery.retry(fn, options);
}