| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| class SelfHealingAPIHub {
|
| constructor(config = {}) {
|
| this.config = {
|
| retryAttempts: config.retryAttempts || 3,
|
| retryDelay: config.retryDelay || 1000,
|
| healthCheckInterval: config.healthCheckInterval || 60000,
|
| cacheExpiry: config.cacheExpiry || 300000,
|
| 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();
|
| }
|
| }
|
|
|
| |
| |
|
|
| startHealthMonitoring() {
|
| console.log('🏥 Self-Healing System: Health monitoring started');
|
|
|
| setInterval(() => {
|
| this.performHealthChecks();
|
| this.cleanupFailedEndpoints();
|
| this.cleanupExpiredCache();
|
| }, this.config.healthCheckInterval);
|
| }
|
|
|
| |
| |
|
|
| async performHealthChecks() {
|
| const endpoints = this.getRegisteredEndpoints();
|
|
|
| for (const endpoint of endpoints) {
|
| if (!this.activeRecoveries.has(endpoint)) {
|
| await this.checkEndpointHealth(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;
|
| }
|
| }
|
|
|
| |
| |
|
|
| async fetchWithRecovery(url, options = {}) {
|
| const cacheKey = `${options.method || 'GET'}:${url}`;
|
|
|
|
|
| if (this.config.enableCaching && options.method === 'GET') {
|
| const cached = this.getFromCache(cacheKey);
|
| if (cached) {
|
| console.log(`💾 Using cached data for: ${url}`);
|
| return cached;
|
| }
|
| }
|
|
|
|
|
| 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();
|
|
|
|
|
| if (this.config.enableCaching && options.method === 'GET') {
|
| this.setCache(cacheKey, data);
|
| }
|
|
|
|
|
| if (this.failedEndpoints.has(url)) {
|
| console.log(`✅ Self-Healing: Recovery successful for ${url}`);
|
| this.failedEndpoints.delete(url);
|
| }
|
|
|
| return { success: true, data, source: 'primary' };
|
| }
|
|
|
|
|
| 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) {
|
|
|
| await this.delay(this.config.retryDelay * Math.pow(2, attempt - 1));
|
| } else {
|
|
|
| return await this.tryFallback(url, options, error);
|
| }
|
| }
|
| }
|
|
|
|
|
| return this.handleFailure(url, options);
|
| }
|
|
|
| |
| |
|
|
| 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}`);
|
|
|
|
|
| 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);
|
| }
|
| }
|
|
|
|
|
| return await this.tryBackendProxy(primaryUrl, options, primaryError);
|
| }
|
|
|
| |
| |
|
|
| 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);
|
| }
|
|
|
|
|
| const cacheKey = `${options.method || 'GET'}:${url}`;
|
| const cached = this.getFromCache(cacheKey, true);
|
|
|
| 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);
|
| }
|
|
|
| |
| |
|
|
| 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)
|
| };
|
| }
|
|
|
| |
| |
|
|
| 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'
|
| });
|
|
|
|
|
| if (record.errors.length > 10) {
|
| record.errors = record.errors.slice(-10);
|
| }
|
|
|
| console.error(`❌ Endpoint failure recorded: ${endpoint} (${record.count} failures)`);
|
| }
|
|
|
| |
| |
|
|
| 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'
|
| ];
|
| }
|
|
|
| |
| |
|
|
| getFallbackEndpoints(url) {
|
| const fallbacks = [];
|
|
|
|
|
| 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'],
|
| };
|
|
|
|
|
| for (const [primary, alternatives] of Object.entries(fallbackMap)) {
|
| if (url.includes(primary)) {
|
|
|
| alternatives.forEach(alt => {
|
| const fallbackUrl = this.transformToFallback(url, alt);
|
| if (fallbackUrl) fallbacks.push(fallbackUrl);
|
| });
|
| }
|
| }
|
|
|
| return fallbacks;
|
| }
|
|
|
| |
| |
|
|
| transformToFallback(originalUrl, fallbackBase) {
|
|
|
|
|
| return null;
|
| }
|
|
|
| |
| |
|
|
| getRegisteredEndpoints() {
|
|
|
| return Array.from(this.healthStatus.keys());
|
| }
|
|
|
| |
| |
|
|
| 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;
|
| }
|
| }
|
|
|
| |
| |
|
|
| 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);
|
| }
|
| }
|
| }
|
|
|
| |
| |
|
|
| cleanupFailedEndpoints() {
|
| const maxAge = 3600000;
|
| 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);
|
| }
|
| }
|
| }
|
|
|
| |
| |
|
|
| 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()
|
| };
|
| }
|
|
|
| |
| |
|
|
| delay(ms) {
|
| return new Promise(resolve => setTimeout(resolve, ms));
|
| }
|
|
|
| |
| |
|
|
| 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);
|
| }
|
| }
|
|
|
| |
| |
|
|
| 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
|
| }
|
| };
|
| }
|
| }
|
|
|
|
|
| if (typeof module !== 'undefined' && module.exports) {
|
| module.exports = SelfHealingAPIHub;
|
| }
|
|
|