polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
524 lines • 19.9 kB
JavaScript
"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