UNPKG

polyv-live-cli

Version:

CLI tool for managing PolyV live streaming services.

524 lines 19.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FallbackManager = void 0; const events_1 = require("events"); class FallbackManager extends events_1.EventEmitter { constructor(config = {}) { super(); this.services = new Map(); this.fallbackRules = new Map(); this.fallbackCache = new Map(); this.isRunning = false; this.requestHistory = new Map(); this.config = { enableAutoFallback: true, maxRetryAttempts: 3, retryDelay: 1000, retryDelayMultiplier: 2, maxRetryDelay: 30000, circuitBreakerThreshold: 5, circuitBreakerTimeout: 60000, healthCheckInterval: 30000, requestTimeout: 5000, enableFallbackCache: true, fallbackCacheTtl: 300000, ...config, }; this.stats = { totalFallbacks: 0, fallbacksByStrategy: { 'cache': 0, 'alternative-endpoint': 0, 'mock-data': 0, 'partial-response': 0, 'offline-mode': 0, }, fallbackSuccessRate: 0, serviceHealth: new Map(), activeRules: 0, circuitBreakerActivations: 0, cacheHitRate: 0, }; this.setupEventHandlers(); } start() { if (this.isRunning) { return; } this.isRunning = true; this.startHealthChecks(); this.emit('fallbackManagerStarted', { timestamp: Date.now(), config: this.config, }); } stop() { if (!this.isRunning) { return; } this.isRunning = false; if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); delete this.healthCheckTimer; } this.emit('fallbackManagerStopped', { timestamp: Date.now(), finalStats: this.getStats(), }); } registerService(serviceId, endpoints) { const serviceEndpoints = endpoints.map(ep => ({ ...ep, healthStatus: 'unknown', lastHealthCheck: 0, responseTime: 0, successRate: 1, circuitBreakerState: 'closed', consecutiveFailures: 0, })); this.services.set(serviceId, serviceEndpoints); this.requestHistory.set(serviceId, []); this.stats.serviceHealth.set(serviceId, { serviceId, healthStatus: 'unknown', endpoints: serviceEndpoints, successRate: 1, averageResponseTime: 0, errorRate: 0, lastSuccessful: 0, totalRequests: 0, failedRequests: 0, circuitBreakerStatus: {}, }); this.emit('serviceRegistered', { serviceId, endpoints: serviceEndpoints.length, timestamp: Date.now(), }); } addFallbackRule(rule) { const serviceRules = this.fallbackRules.get(rule.serviceId) || []; serviceRules.push(rule); serviceRules.sort((a, b) => a.priority - b.priority); this.fallbackRules.set(rule.serviceId, serviceRules); this.stats.activeRules = Array.from(this.fallbackRules.values()) .flat() .filter(r => r.enabled).length; this.emit('fallbackRuleAdded', { ruleId: rule.id, serviceId: rule.serviceId, strategy: rule.strategy, timestamp: Date.now(), }); } removeFallbackRule(serviceId, ruleId) { const serviceRules = this.fallbackRules.get(serviceId) || []; const filteredRules = serviceRules.filter(rule => rule.id !== ruleId); this.fallbackRules.set(serviceId, filteredRules); this.stats.activeRules = Array.from(this.fallbackRules.values()) .flat() .filter(r => r.enabled).length; this.emit('fallbackRuleRemoved', { ruleId, serviceId, timestamp: Date.now(), }); } async executeWithFallback(serviceId, requestFn, options = {}) { const startTime = Date.now(); try { if (options.enableCache !== false && options.cacheKey) { const cachedResult = this.getCachedResult(options.cacheKey); if (cachedResult) { return { success: true, strategy: 'cache', data: cachedResult, responseTime: Date.now() - startTime, source: 'cache', }; } } const primaryEndpoint = this.getPrimaryEndpoint(serviceId); if (primaryEndpoint && this.canUseEndpoint(primaryEndpoint)) { try { const result = await this.executeRequest(primaryEndpoint, requestFn, options.timeout); this.recordSuccess(serviceId, primaryEndpoint.id, Date.now() - startTime); if (this.config.enableFallbackCache && options.cacheKey) { this.cacheResult(options.cacheKey, result); } return { success: true, strategy: 'cache', data: result, responseTime: Date.now() - startTime, source: 'primary', }; } catch (error) { this.recordFailure(serviceId, primaryEndpoint.id); } } return await this.executeFallback(serviceId, requestFn, options); } catch (error) { return { success: false, strategy: 'cache', data: null, responseTime: Date.now() - startTime, error: error instanceof Error ? error : new Error('Unknown error'), source: 'fallback', }; } } getServiceHealth(serviceId) { return this.stats.serviceHealth.get(serviceId) || null; } getStats() { return { ...this.stats }; } openCircuitBreaker(serviceId, endpointId) { const endpoints = this.services.get(serviceId); if (endpoints) { const endpoint = endpoints.find(ep => ep.id === endpointId); if (endpoint) { endpoint.circuitBreakerState = 'open'; this.stats.circuitBreakerActivations++; this.emit('circuitBreakerOpened', { serviceId, endpointId, timestamp: Date.now(), }); setTimeout(() => { if (endpoint.circuitBreakerState === 'open') { endpoint.circuitBreakerState = 'half-open'; this.emit('circuitBreakerHalfOpen', { serviceId, endpointId, timestamp: Date.now(), }); } }, this.config.circuitBreakerTimeout); } } } closeCircuitBreaker(serviceId, endpointId) { const endpoints = this.services.get(serviceId); if (endpoints) { const endpoint = endpoints.find(ep => ep.id === endpointId); if (endpoint) { endpoint.circuitBreakerState = 'closed'; endpoint.consecutiveFailures = 0; this.emit('circuitBreakerClosed', { serviceId, endpointId, timestamp: Date.now(), }); } } } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.emit('configUpdated', { config: this.config, timestamp: Date.now(), }); } clearCache() { const cacheSize = this.fallbackCache.size; this.fallbackCache.clear(); this.emit('cacheCleared', { entriesCleared: cacheSize, timestamp: Date.now(), }); } getPrimaryEndpoint(serviceId) { const endpoints = this.services.get(serviceId); if (!endpoints) return null; return endpoints .filter(ep => ep.isPrimary) .sort((a, b) => a.priority - b.priority)[0] || null; } canUseEndpoint(endpoint) { return endpoint.circuitBreakerState !== 'open' && endpoint.healthStatus !== 'unhealthy'; } async executeRequest(endpoint, requestFn, timeout) { const requestTimeout = timeout || this.config.requestTimeout; return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Request timeout after ${requestTimeout}ms`)); }, requestTimeout); requestFn(endpoint) .then(result => { clearTimeout(timer); resolve(result); }) .catch(error => { clearTimeout(timer); reject(error); }); }); } async executeFallback(serviceId, requestFn, options) { this.stats.totalFallbacks++; const startTime = Date.now(); const applicableRules = this.getApplicableFallbackRules(serviceId); for (const rule of applicableRules) { try { const result = await this.executeFallbackStrategy(rule, serviceId, requestFn, options); this.stats.fallbacksByStrategy[rule.strategy]++; return { success: true, strategy: rule.strategy, data: result, responseTime: Date.now() - startTime, source: 'fallback', }; } catch (error) { continue; } } throw new Error('All fallback strategies failed'); } getApplicableFallbackRules(serviceId) { const rules = this.fallbackRules.get(serviceId) || []; return rules .filter(rule => rule.enabled && this.isRuleTriggered(rule)) .sort((a, b) => a.priority - b.priority); } isRuleTriggered(rule) { const history = this.requestHistory.get(rule.serviceId) || []; const now = Date.now(); for (const trigger of rule.triggers) { const relevantHistory = history.filter(req => now - req.timestamp <= trigger.timeWindow); if (relevantHistory.length < trigger.minSamples) { continue; } switch (trigger.type) { case 'error-rate': { const errorRate = relevantHistory.filter(req => !req.success).length / relevantHistory.length; if (errorRate >= trigger.threshold) return true; break; } case 'response-time': { const avgResponseTime = relevantHistory.reduce((sum, req) => sum + req.responseTime, 0) / relevantHistory.length; if (avgResponseTime >= trigger.threshold) return true; break; } case 'circuit-breaker': { const endpoints = this.services.get(rule.serviceId) || []; const openCircuits = endpoints.filter(ep => ep.circuitBreakerState === 'open').length; if (openCircuits >= trigger.threshold) return true; break; } } } return false; } async executeFallbackStrategy(rule, serviceId, requestFn, options) { switch (rule.strategy) { case 'cache': return this.executeCacheFallback(rule, options); case 'alternative-endpoint': return this.executeAlternativeEndpointFallback(rule, serviceId, requestFn, options); case 'mock-data': return this.executeMockDataFallback(rule, options); case 'partial-response': return this.executePartialResponseFallback(rule, options); case 'offline-mode': return this.executeOfflineModeFallback(rule, options); default: throw new Error(`Unknown fallback strategy: ${rule.strategy}`); } } async executeCacheFallback(_rule, options) { if (!options.cacheKey) { throw new Error('Cache key required for cache fallback'); } const cachedResult = this.getCachedResult(options.cacheKey, true); if (cachedResult) { return cachedResult; } throw new Error('No cached data available'); } async executeAlternativeEndpointFallback(_rule, serviceId, requestFn, options) { const endpoints = this.services.get(serviceId) || []; const alternativeEndpoints = endpoints .filter(ep => !ep.isPrimary && this.canUseEndpoint(ep)) .sort((a, b) => a.priority - b.priority); for (const endpoint of alternativeEndpoints) { try { const result = await this.executeRequest(endpoint, requestFn, options.timeout); this.recordSuccess(serviceId, endpoint.id, 0); return result; } catch (error) { this.recordFailure(serviceId, endpoint.id); continue; } } throw new Error('All alternative endpoints failed'); } async executeMockDataFallback(rule, options) { const mockData = options['mockData'] || rule.config['mockData']; if (!mockData) { throw new Error('No mock data available'); } return mockData; } async executePartialResponseFallback(rule, _options) { return { status: 'partial', message: 'Some data may be unavailable due to service issues', data: rule.config['partialData'] || {}, timestamp: Date.now(), }; } async executeOfflineModeFallback(rule, _options) { return { status: 'offline', message: 'Operating in offline mode', data: rule.config['offlineData'] || null, timestamp: Date.now(), }; } getCachedResult(cacheKey, allowStale = false) { const cached = this.fallbackCache.get(cacheKey); if (!cached) return null; const isExpired = Date.now() - cached.timestamp > cached.ttl; if (isExpired && !allowStale) { this.fallbackCache.delete(cacheKey); return null; } return cached.data; } cacheResult(cacheKey, data, ttl) { this.fallbackCache.set(cacheKey, { data, timestamp: Date.now(), ttl: ttl || this.config.fallbackCacheTtl, }); } recordSuccess(serviceId, endpointId, responseTime) { const history = this.requestHistory.get(serviceId) || []; history.push({ timestamp: Date.now(), success: true, responseTime, }); this.requestHistory.set(serviceId, history.slice(-100)); const endpoints = this.services.get(serviceId); if (endpoints) { const endpoint = endpoints.find(ep => ep.id === endpointId); if (endpoint) { endpoint.consecutiveFailures = 0; endpoint.responseTime = responseTime; endpoint.lastHealthCheck = Date.now(); endpoint.healthStatus = 'healthy'; if (endpoint.circuitBreakerState === 'half-open') { this.closeCircuitBreaker(serviceId, endpointId); } } } this.updateServiceHealthMetrics(serviceId); } recordFailure(serviceId, endpointId) { const history = this.requestHistory.get(serviceId) || []; history.push({ timestamp: Date.now(), success: false, responseTime: 0, }); this.requestHistory.set(serviceId, history.slice(-100)); const endpoints = this.services.get(serviceId); if (endpoints) { const endpoint = endpoints.find(ep => ep.id === endpointId); if (endpoint) { endpoint.consecutiveFailures++; endpoint.healthStatus = 'unhealthy'; if (endpoint.consecutiveFailures >= this.config.circuitBreakerThreshold) { this.openCircuitBreaker(serviceId, endpointId); } } } this.updateServiceHealthMetrics(serviceId); } updateServiceHealthMetrics(serviceId) { const history = this.requestHistory.get(serviceId) || []; const recentHistory = history.filter(req => Date.now() - req.timestamp <= 300000); const healthMetrics = this.stats.serviceHealth.get(serviceId); if (healthMetrics) { healthMetrics.totalRequests = history.length; healthMetrics.failedRequests = history.filter(req => !req.success).length; healthMetrics.successRate = recentHistory.length > 0 ? recentHistory.filter(req => req.success).length / recentHistory.length : 1; healthMetrics.errorRate = 1 - healthMetrics.successRate; healthMetrics.averageResponseTime = recentHistory.length > 0 ? recentHistory.reduce((sum, req) => sum + req.responseTime, 0) / recentHistory.length : 0; const lastSuccessful = recentHistory.find(req => req.success); if (lastSuccessful) { healthMetrics.lastSuccessful = lastSuccessful.timestamp; } if (healthMetrics.successRate >= 0.95) { healthMetrics.healthStatus = 'healthy'; } else if (healthMetrics.successRate >= 0.8) { healthMetrics.healthStatus = 'degraded'; } else { healthMetrics.healthStatus = 'unhealthy'; } } } startHealthChecks() { this.healthCheckTimer = setInterval(() => { this.performHealthChecks(); }, this.config.healthCheckInterval); } async performHealthChecks() { for (const [serviceId, endpoints] of this.services) { for (const endpoint of endpoints) { try { const startTime = Date.now(); endpoint.responseTime = Date.now() - startTime; endpoint.lastHealthCheck = Date.now(); if (endpoint.circuitBreakerState === 'half-open') { endpoint.healthStatus = 'healthy'; this.closeCircuitBreaker(serviceId, endpoint.id); } } catch (error) { endpoint.healthStatus = 'unhealthy'; } } } this.emit('healthCheckCompleted', { timestamp: Date.now(), }); } setupEventHandlers() { setInterval(() => { this.cleanupExpiredCache(); }, 60000); } cleanupExpiredCache() { const now = Date.now(); for (const [key, cached] of this.fallbackCache) { if (now - cached.timestamp > cached.ttl) { this.fallbackCache.delete(key); } } } } exports.FallbackManager = FallbackManager; //# sourceMappingURL=fallback-manager.js.map