/** * Circuit Breaker pour éviter de surcharger Komga quand il est défaillant * Évite l'effet avalanche en coupant les requêtes vers un service défaillant */ import type { CircuitBreakerConfig } from "@/types/preferences"; import logger from "@/lib/logger"; interface CircuitBreakerState { state: 'CLOSED' | 'OPEN' | 'HALF_OPEN'; failureCount: number; lastFailureTime: number; nextAttemptTime: number; } class CircuitBreaker { private state: CircuitBreakerState = { state: 'CLOSED', failureCount: 0, lastFailureTime: 0, nextAttemptTime: 0, }; private config = { failureThreshold: 5, // Nombre d'échecs avant ouverture recoveryTimeout: 30000, // 30s avant tentative de récupération resetTimeout: 60000, // Délai de reset après échec }; private getConfigFromPreferences: (() => Promise) | null = null; /** * Configure une fonction pour récupérer dynamiquement la config depuis les préférences */ setConfigGetter(getter: () => Promise): void { this.getConfigFromPreferences = getter; } /** * Récupère la config actuelle, soit depuis les préférences, soit depuis les valeurs par défaut */ private async getCurrentConfig(): Promise { if (this.getConfigFromPreferences) { try { const prefConfig = await this.getConfigFromPreferences(); return { failureThreshold: prefConfig.threshold ?? 5, recoveryTimeout: prefConfig.timeout ?? 30000, resetTimeout: prefConfig.resetTimeout ?? 60000, }; } catch (error) { logger.error({ err: error }, 'Error getting circuit breaker config from preferences'); return this.config; } } return this.config; } async execute(operation: () => Promise): Promise { const config = await this.getCurrentConfig(); if (this.state.state === 'OPEN') { if (Date.now() < this.state.nextAttemptTime) { throw new Error('Circuit breaker is OPEN - Komga service unavailable'); } this.state.state = 'HALF_OPEN'; } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { await this.onFailure(config); throw error; } } private onSuccess(): void { if (this.state.state === 'HALF_OPEN') { this.state.failureCount = 0; this.state.state = 'CLOSED'; logger.info('[CIRCUIT-BREAKER] ✅ Circuit closed - Komga recovered'); } } private async onFailure(config: typeof this.config): Promise { this.state.failureCount++; this.state.lastFailureTime = Date.now(); if (this.state.failureCount >= config.failureThreshold) { this.state.state = 'OPEN'; this.state.nextAttemptTime = Date.now() + config.resetTimeout; logger.warn(`[CIRCUIT-BREAKER] 🔴 Circuit OPEN - Komga failing (${this.state.failureCount} failures, reset in ${config.resetTimeout}ms)`); } } getState(): CircuitBreakerState { return { ...this.state }; } reset(): void { this.state = { state: 'CLOSED', failureCount: 0, lastFailureTime: 0, nextAttemptTime: 0, }; logger.info('[CIRCUIT-BREAKER] 🔄 Circuit reset'); } } export const CircuitBreakerService = new CircuitBreaker();