import type { TCircuitState, IUpstreamResilienceConfig } from './interfaces.upstream.js'; import { DEFAULT_RESILIENCE_CONFIG } from './interfaces.upstream.js'; /** * Circuit breaker implementation for upstream resilience. * * States: * - CLOSED: Normal operation, requests pass through * - OPEN: Circuit is tripped, requests fail fast * - HALF_OPEN: Testing if upstream has recovered * * Transitions: * - CLOSED → OPEN: When failure count exceeds threshold * - OPEN → HALF_OPEN: After reset timeout expires * - HALF_OPEN → CLOSED: On successful request * - HALF_OPEN → OPEN: On failed request */ export class CircuitBreaker { /** Unique identifier for logging and metrics */ public readonly id: string; /** Current circuit state */ private state: TCircuitState = 'CLOSED'; /** Count of consecutive failures */ private failureCount: number = 0; /** Timestamp when circuit was opened */ private openedAt: number = 0; /** Number of successful requests in half-open state */ private halfOpenSuccesses: number = 0; /** Configuration */ private readonly config: IUpstreamResilienceConfig; /** Number of successes required to close circuit from half-open */ private readonly halfOpenThreshold: number = 2; constructor(id: string, config?: Partial) { this.id = id; this.config = { ...DEFAULT_RESILIENCE_CONFIG, ...config }; } /** * Get current circuit state. */ public getState(): TCircuitState { // Check if we should transition from OPEN to HALF_OPEN if (this.state === 'OPEN') { const elapsed = Date.now() - this.openedAt; if (elapsed >= this.config.circuitBreakerResetMs) { this.transitionTo('HALF_OPEN'); } } return this.state; } /** * Check if circuit allows requests. * Returns true if requests should be allowed. */ public canRequest(): boolean { const currentState = this.getState(); return currentState !== 'OPEN'; } /** * Record a successful request. * May transition circuit from HALF_OPEN to CLOSED. */ public recordSuccess(): void { if (this.state === 'HALF_OPEN') { this.halfOpenSuccesses++; if (this.halfOpenSuccesses >= this.halfOpenThreshold) { this.transitionTo('CLOSED'); } } else if (this.state === 'CLOSED') { // Reset failure count on success this.failureCount = 0; } } /** * Record a failed request. * May transition circuit from CLOSED/HALF_OPEN to OPEN. */ public recordFailure(): void { if (this.state === 'HALF_OPEN') { // Any failure in half-open immediately opens circuit this.transitionTo('OPEN'); } else if (this.state === 'CLOSED') { this.failureCount++; if (this.failureCount >= this.config.circuitBreakerThreshold) { this.transitionTo('OPEN'); } } } /** * Force circuit to open state. * Useful for manual intervention or external health checks. */ public forceOpen(): void { this.transitionTo('OPEN'); } /** * Force circuit to closed state. * Useful for manual intervention after fixing upstream issues. */ public forceClose(): void { this.transitionTo('CLOSED'); } /** * Reset circuit to initial state. */ public reset(): void { this.state = 'CLOSED'; this.failureCount = 0; this.openedAt = 0; this.halfOpenSuccesses = 0; } /** * Get circuit metrics for monitoring. */ public getMetrics(): ICircuitBreakerMetrics { return { id: this.id, state: this.getState(), failureCount: this.failureCount, openedAt: this.openedAt > 0 ? new Date(this.openedAt) : null, timeUntilHalfOpen: this.state === 'OPEN' ? Math.max(0, this.config.circuitBreakerResetMs - (Date.now() - this.openedAt)) : 0, halfOpenSuccesses: this.halfOpenSuccesses, threshold: this.config.circuitBreakerThreshold, resetMs: this.config.circuitBreakerResetMs, }; } /** * Transition to a new state with proper cleanup. */ private transitionTo(newState: TCircuitState): void { const previousState = this.state; this.state = newState; switch (newState) { case 'OPEN': this.openedAt = Date.now(); this.halfOpenSuccesses = 0; break; case 'HALF_OPEN': this.halfOpenSuccesses = 0; break; case 'CLOSED': this.failureCount = 0; this.openedAt = 0; this.halfOpenSuccesses = 0; break; } // Log state transition (useful for debugging and monitoring) // In production, this would emit events or metrics if (previousState !== newState) { // State changed - could emit event here } } } /** * Metrics for circuit breaker monitoring. */ export interface ICircuitBreakerMetrics { /** Circuit breaker identifier */ id: string; /** Current state */ state: TCircuitState; /** Number of consecutive failures */ failureCount: number; /** When circuit was opened (null if never opened) */ openedAt: Date | null; /** Milliseconds until circuit transitions to half-open (0 if not open) */ timeUntilHalfOpen: number; /** Number of successes in half-open state */ halfOpenSuccesses: number; /** Failure threshold for opening circuit */ threshold: number; /** Reset timeout in milliseconds */ resetMs: number; } /** * Execute a function with circuit breaker protection. * * @param breaker The circuit breaker to use * @param fn The async function to execute * @param fallback Optional fallback function when circuit is open * @returns The result of fn or fallback * @throws CircuitOpenError if circuit is open and no fallback provided */ export async function withCircuitBreaker( breaker: CircuitBreaker, fn: () => Promise, fallback?: () => Promise, ): Promise { if (!breaker.canRequest()) { if (fallback) { return fallback(); } throw new CircuitOpenError(breaker.id); } try { const result = await fn(); breaker.recordSuccess(); return result; } catch (error) { breaker.recordFailure(); throw error; } } /** * Error thrown when circuit is open and no fallback is provided. */ export class CircuitOpenError extends Error { public readonly circuitId: string; constructor(circuitId: string) { super(`Circuit breaker '${circuitId}' is open`); this.name = 'CircuitOpenError'; this.circuitId = circuitId; } }