/** * Crypto API Hub Self-Healing Module * * This module provides automatic recovery, fallback mechanisms, * and health monitoring for the Crypto API Hub dashboard. * * Features: * - Automatic API health checks * - Fallback to alternative endpoints * - Retry logic with exponential backoff * - Data caching for offline resilience * - Automatic error recovery */ class SelfHealingAPIHub { constructor(config = {}) { this.config = { retryAttempts: config.retryAttempts || 3, retryDelay: config.retryDelay || 1000, healthCheckInterval: config.healthCheckInterval || 60000, // 1 minute cacheExpiry: config.cacheExpiry || 300000, // 5 minutes backendUrl: config.backendUrl || '/api', enableAutoRecovery: config.enableAutoRecovery !== false, enableCaching: config.enableCaching !== false, ...config }; this.cache = new Map(); this.healthStatus = new Map(); this.failedEndpoints = new Map(); this.activeRecoveries = new Set(); if (this.config.enableAutoRecovery) { this.startHealthMonitoring(); } } /** * Start continuous health monitoring */ startHealthMonitoring() { console.log('๐Ÿฅ Self-Healing System: Health monitoring started'); setInterval(() => { this.performHealthChecks(); this.cleanupFailedEndpoints(); this.cleanupExpiredCache(); }, this.config.healthCheckInterval); } /** * Perform health checks on all registered endpoints */ async performHealthChecks() { const endpoints = this.getRegisteredEndpoints(); for (const endpoint of endpoints) { if (!this.activeRecoveries.has(endpoint)) { await this.checkEndpointHealth(endpoint); } } } /** * Check health of a specific endpoint */ async checkEndpointHealth(endpoint) { try { const response = await this.fetchWithTimeout(endpoint, { method: 'HEAD', timeout: 5000 }); this.healthStatus.set(endpoint, { status: response.ok ? 'healthy' : 'degraded', lastCheck: Date.now(), responseTime: response.headers.get('X-Response-Time') || 'N/A' }); if (response.ok && this.failedEndpoints.has(endpoint)) { console.log(`โœ… Self-Healing: Endpoint recovered: ${endpoint}`); this.failedEndpoints.delete(endpoint); } return response.ok; } catch (error) { this.healthStatus.set(endpoint, { status: 'unhealthy', lastCheck: Date.now(), error: error.message }); this.recordFailure(endpoint, error); return false; } } /** * Fetch with automatic retry and fallback */ async fetchWithRecovery(url, options = {}) { const cacheKey = `${options.method || 'GET'}:${url}`; // Try cache first if enabled if (this.config.enableCaching && options.method === 'GET') { const cached = this.getFromCache(cacheKey); if (cached) { console.log(`๐Ÿ’พ Using cached data for: ${url}`); return cached; } } // Try primary endpoint with retry for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) { try { const response = await this.fetchWithTimeout(url, options); if (response.ok) { const data = await response.json(); // Cache successful response if (this.config.enableCaching && options.method === 'GET') { this.setCache(cacheKey, data); } // Clear any failure records if (this.failedEndpoints.has(url)) { console.log(`โœ… Self-Healing: Recovery successful for ${url}`); this.failedEndpoints.delete(url); } return { success: true, data, source: 'primary' }; } // If response not OK, try fallback on last attempt if (attempt === this.config.retryAttempts) { return await this.tryFallback(url, options); } } catch (error) { console.warn(`โš ๏ธ Attempt ${attempt}/${this.config.retryAttempts} failed for ${url}:`, error.message); if (attempt < this.config.retryAttempts) { // Exponential backoff await this.delay(this.config.retryDelay * Math.pow(2, attempt - 1)); } else { // Last attempt - try fallback return await this.tryFallback(url, options, error); } } } // All attempts failed return this.handleFailure(url, options); } /** * Try fallback endpoints */ async tryFallback(primaryUrl, options = {}, primaryError = null) { console.log(`๐Ÿ”„ Self-Healing: Attempting fallback for ${primaryUrl}`); const fallbacks = this.getFallbackEndpoints(primaryUrl); for (const fallbackUrl of fallbacks) { try { const response = await this.fetchWithTimeout(fallbackUrl, options); if (response.ok) { const data = await response.json(); console.log(`โœ… Self-Healing: Fallback successful using ${fallbackUrl}`); // Cache fallback data const cacheKey = `${options.method || 'GET'}:${primaryUrl}`; this.setCache(cacheKey, data); return { success: true, data, source: 'fallback', fallbackUrl }; } } catch (error) { console.warn(`โš ๏ธ Fallback attempt failed for ${fallbackUrl}:`, error.message); } } // No fallback worked - try backend proxy return await this.tryBackendProxy(primaryUrl, options, primaryError); } /** * Try backend proxy as last resort */ async tryBackendProxy(url, options = {}, originalError = null) { console.log(`๐Ÿ”„ Self-Healing: Attempting backend proxy for ${url}`); try { const proxyUrl = `${this.config.backendUrl}/proxy`; const response = await fetch(proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url, method: options.method || 'GET', headers: options.headers || {}, body: options.body }) }); if (response.ok) { const data = await response.json(); console.log(`โœ… Self-Healing: Backend proxy successful`); return { success: true, data, source: 'backend-proxy' }; } } catch (error) { console.error(`โŒ Backend proxy failed:`, error); } // Everything failed - return cached data if available const cacheKey = `${options.method || 'GET'}:${url}`; const cached = this.getFromCache(cacheKey, true); // Get even expired cache if (cached) { console.log(`๐Ÿ’พ Self-Healing: Using stale cache as last resort`); return { success: true, data: cached, source: 'stale-cache', warning: 'Data may be outdated' }; } return this.handleFailure(url, options, originalError); } /** * Handle complete failure */ handleFailure(url, options, error) { this.recordFailure(url, error); return { success: false, error: error?.message || 'All recovery attempts failed', url, timestamp: Date.now(), recoveryAttempts: this.config.retryAttempts, suggestions: this.getRecoverySuggestions(url) }; } /** * Record endpoint failure */ recordFailure(endpoint, error) { if (!this.failedEndpoints.has(endpoint)) { this.failedEndpoints.set(endpoint, { count: 0, firstFailure: Date.now(), errors: [] }); } const record = this.failedEndpoints.get(endpoint); record.count++; record.lastFailure = Date.now(); record.errors.push({ timestamp: Date.now(), message: error?.message || 'Unknown error' }); // Keep only last 10 errors if (record.errors.length > 10) { record.errors = record.errors.slice(-10); } console.error(`โŒ Endpoint failure recorded: ${endpoint} (${record.count} failures)`); } /** * Get recovery suggestions */ getRecoverySuggestions(url) { return [ 'Check your internet connection', 'Verify API key is valid and not expired', 'Check if API service is operational', 'Try again in a few moments', 'Consider using alternative data sources' ]; } /** * Get fallback endpoints for a given URL */ getFallbackEndpoints(url) { const fallbacks = []; // Define fallback mappings const fallbackMap = { 'etherscan.io': ['blockchair.com/ethereum', 'ethplorer.io'], 'bscscan.com': ['api.bscscan.com'], 'coingecko.com': ['api.coinpaprika.com', 'api.coincap.io'], 'coinmarketcap.com': ['api.coingecko.com', 'api.coinpaprika.com'], 'cryptopanic.com': ['newsapi.org'], }; // Find matching fallbacks for (const [primary, alternatives] of Object.entries(fallbackMap)) { if (url.includes(primary)) { // Transform URL to fallback format alternatives.forEach(alt => { const fallbackUrl = this.transformToFallback(url, alt); if (fallbackUrl) fallbacks.push(fallbackUrl); }); } } return fallbacks; } /** * Transform URL to fallback format */ transformToFallback(originalUrl, fallbackBase) { // This is a simplified transformation // In production, you'd need more sophisticated URL transformation logic return null; // Override in specific implementations } /** * Get registered endpoints */ getRegisteredEndpoints() { // This should be populated with actual endpoints from SERVICES object return Array.from(this.healthStatus.keys()); } /** * Fetch with timeout */ async fetchWithTimeout(url, options = {}) { const timeout = options.timeout || 10000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error(`Request timeout after ${timeout}ms`); } throw error; } } /** * Cache management */ setCache(key, data) { this.cache.set(key, { data, timestamp: Date.now(), expiry: Date.now() + this.config.cacheExpiry }); } getFromCache(key, allowExpired = false) { const cached = this.cache.get(key); if (!cached) return null; if (allowExpired || cached.expiry > Date.now()) { return cached.data; } return null; } cleanupExpiredCache() { const now = Date.now(); for (const [key, value] of this.cache.entries()) { if (value.expiry < now) { this.cache.delete(key); } } } /** * Clean up old failed endpoints */ cleanupFailedEndpoints() { const maxAge = 3600000; // 1 hour const now = Date.now(); for (const [endpoint, record] of this.failedEndpoints.entries()) { if (now - record.lastFailure > maxAge) { console.log(`๐Ÿงน Cleaning up old failure record: ${endpoint}`); this.failedEndpoints.delete(endpoint); } } } /** * Get system health status */ getHealthStatus() { const total = this.healthStatus.size; const healthy = Array.from(this.healthStatus.values()).filter(s => s.status === 'healthy').length; const degraded = Array.from(this.healthStatus.values()).filter(s => s.status === 'degraded').length; const unhealthy = Array.from(this.healthStatus.values()).filter(s => s.status === 'unhealthy').length; return { total, healthy, degraded, unhealthy, healthPercentage: total > 0 ? Math.round((healthy / total) * 100) : 0, failedEndpoints: this.failedEndpoints.size, cacheSize: this.cache.size, lastCheck: Date.now() }; } /** * Utility: Delay */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Manual recovery trigger */ async triggerRecovery(endpoint) { console.log(`๐Ÿ”ง Manual recovery triggered for: ${endpoint}`); this.activeRecoveries.add(endpoint); try { const isHealthy = await this.checkEndpointHealth(endpoint); if (isHealthy) { this.failedEndpoints.delete(endpoint); return { success: true, message: 'Endpoint recovered' }; } else { return { success: false, message: 'Endpoint still unhealthy' }; } } finally { this.activeRecoveries.delete(endpoint); } } /** * Get diagnostics information */ getDiagnostics() { return { health: this.getHealthStatus(), failedEndpoints: Array.from(this.failedEndpoints.entries()).map(([url, record]) => ({ url, ...record })), cache: { size: this.cache.size, entries: Array.from(this.cache.keys()) }, config: { retryAttempts: this.config.retryAttempts, retryDelay: this.config.retryDelay, healthCheckInterval: this.config.healthCheckInterval, cacheExpiry: this.config.cacheExpiry, enableAutoRecovery: this.config.enableAutoRecovery, enableCaching: this.config.enableCaching } }; } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = SelfHealingAPIHub; }