"use strict";
/**
 * Circuit breaker implementation for handling external module failures
 * Prevents cascading failures and provides graceful degradation
 *
 * Resource Management:
 * - Tracks all active timers for proper cleanup
 * - Provides shutdown() method to cancel in-flight operations
 * - Prevents timer leaks in retry operations
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.CircuitBreakerManager = exports.CircuitBreaker = void 0;
/**
 * Circuit breaker for external module dependencies
 */
class CircuitBreaker {
    constructor(options = {}) {
        this.failureTimes = [];
        this.activeTimers = new Set();
        this.pendingRejects = new Set();
        this.isShuttingDown = false;
        this.options = {
            failureThreshold: options.failureThreshold ?? 5,
            recoveryTimeout: options.recoveryTimeout ?? 30000, // 30 seconds
            monitoringPeriod: options.monitoringPeriod ?? 60000, // 1 minute
            exponentialBackoff: options.exponentialBackoff ?? true,
            maxBackoffTime: options.maxBackoffTime ?? 300000, // 5 minutes
            jitterEnabled: options.jitterEnabled ?? true,
        };
        this.state = {
            state: 'closed',
            failures: 0,
            lastFailureTime: 0,
            nextRetryTime: 0,
            consecutiveSuccesses: 0,
            totalRequests: 0,
            totalFailures: 0,
        };
    }
    /**
     * Execute an operation with circuit breaker protection
     */
    async execute(operation, fallback) {
        if (this.isShuttingDown) {
            throw new Error('Circuit breaker is shutting down');
        }
        this.state.totalRequests++;
        if (this.isOpen()) {
            if (fallback) {
                return fallback();
            }
            throw new Error(`Circuit breaker is OPEN. Next retry at ${new Date(this.state.nextRetryTime).toISOString()}`);
        }
        if (this.isHalfOpen()) {
            // In half-open state, allow limited requests
            return this.attemptOperation(operation, fallback);
        }
        // Closed state - normal operation
        return this.attemptOperation(operation, fallback);
    }
    /**
     * Check if shutting down and throw error if so
     */
    checkShutdown() {
        if (this.isShuttingDown) {
            throw new Error('Circuit breaker is shutting down');
        }
    }
    /**
     * Calculate retry delay with exponential backoff and jitter
     */
    calculateRetryDelay(attempt, options) {
        let delay = options.exponential
            ? options.initialDelay * Math.pow(2, attempt)
            : options.initialDelay;
        delay = Math.min(delay, options.maxDelay);
        if (options.jitter) {
            delay *= 0.5 + Math.random() * 0.5;
        }
        return delay;
    }
    /**
     * Execute with retry logic
     */
    async executeWithRetry(operation, retryOptions = {}, fallback) {
        this.checkShutdown();
        const options = {
            maxRetries: retryOptions.maxRetries ?? 3,
            initialDelay: retryOptions.initialDelay ?? 1000,
            maxDelay: retryOptions.maxDelay ?? 10000,
            exponential: retryOptions.exponential ?? true,
            jitter: retryOptions.jitter ?? true,
        };
        let lastError = null;
        for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
            this.checkShutdown();
            try {
                return await this.execute(operation, fallback);
            }
            catch (error) {
                lastError = error;
                this.checkShutdown();
                if (attempt === options.maxRetries) {
                    break;
                }
                const delay = this.calculateRetryDelay(attempt, options);
                await this.sleep(delay);
            }
        }
        if (fallback) {
            return fallback();
        }
        throw lastError || new Error('All retry attempts failed');
    }
    async attemptOperation(operation, fallback) {
        try {
            const result = await operation();
            this.recordSuccess();
            return result;
        }
        catch (error) {
            this.recordFailure();
            if (fallback && this.isOpen()) {
                return fallback();
            }
            throw error;
        }
    }
    recordSuccess() {
        this.state.consecutiveSuccesses++;
        this.cleanupOldFailures();
        if (this.state.state === 'half-open') {
            // Successful request in half-open state - close the circuit
            if (this.state.consecutiveSuccesses >= 3) {
                this.state.state = 'closed';
                this.state.failures = 0;
                this.state.consecutiveSuccesses = 0;
                this.failureTimes.length = 0;
            }
        }
    }
    recordFailure() {
        this.state.totalFailures++;
        this.state.failures++;
        this.state.consecutiveSuccesses = 0;
        this.state.lastFailureTime = Date.now();
        this.failureTimes.push(Date.now());
        this.cleanupOldFailures();
        // Check if we should open the circuit
        if (this.state.failures >= this.options.failureThreshold) {
            this.openCircuit();
        }
    }
    openCircuit() {
        this.state.state = 'open';
        // Calculate next retry time with exponential backoff
        let backoffTime = this.options.recoveryTimeout;
        if (this.options.exponentialBackoff) {
            const failureCount = Math.min(this.state.failures - this.options.failureThreshold, 10);
            backoffTime = Math.min(this.options.recoveryTimeout * Math.pow(2, failureCount), this.options.maxBackoffTime);
        }
        // Add jitter to prevent thundering herd
        if (this.options.jitterEnabled) {
            backoffTime *= 0.5 + Math.random() * 0.5;
        }
        this.state.nextRetryTime = Date.now() + backoffTime;
    }
    cleanupOldFailures() {
        const cutoff = Date.now() - this.options.monitoringPeriod;
        const validFailures = this.failureTimes.filter(time => time > cutoff);
        this.failureTimes.length = 0;
        this.failureTimes.push(...validFailures);
        this.state.failures = this.failureTimes.length;
    }
    isOpen() {
        if (this.state.state !== 'open') {
            return false;
        }
        // Check if recovery timeout has passed
        if (Date.now() >= this.state.nextRetryTime) {
            this.state.state = 'half-open';
            this.state.consecutiveSuccesses = 0;
            return false;
        }
        return true;
    }
    isHalfOpen() {
        return this.state.state === 'half-open';
    }
    sleep(ms) {
        return new Promise((resolve, reject) => {
            if (this.isShuttingDown) {
                reject(new Error('Circuit breaker is shutting down'));
                return;
            }
            // Track this reject function so we can call it on shutdown
            this.pendingRejects.add(reject);
            const timer = setTimeout(() => {
                this.activeTimers.delete(timer);
                this.pendingRejects.delete(reject);
                resolve();
            }, ms);
            this.activeTimers.add(timer);
        });
    }
    /**
     * Get current circuit breaker state
     */
    getState() {
        this.cleanupOldFailures();
        return { ...this.state };
    }
    /**
     * Get failure rate over the monitoring period
     */
    getFailureRate() {
        if (this.state.totalRequests === 0) {
            return 0;
        }
        return this.state.failures / this.state.totalRequests;
    }
    /**
     * Force circuit to closed state (for testing/admin)
     */
    reset() {
        this.state = {
            state: 'closed',
            failures: 0,
            lastFailureTime: 0,
            nextRetryTime: 0,
            consecutiveSuccesses: 0,
            totalRequests: this.state.totalRequests, // Keep request count
            totalFailures: this.state.totalFailures, // Keep failure count
        };
        this.failureTimes.length = 0;
    }
    /**
     * Force circuit to open state (for maintenance/emergency)
     */
    forceOpen(duration = this.options.recoveryTimeout) {
        this.state.state = 'open';
        this.state.nextRetryTime = Date.now() + duration;
    }
    /**
     * Get health status of the circuit breaker
     */
    getHealthStatus() {
        const failureRate = this.getFailureRate();
        return {
            healthy: this.state.state === 'closed' && failureRate < 0.1,
            state: this.state.state,
            failureRate,
            failures: this.state.failures,
            nextRetry: this.state.state === 'open' ? new Date(this.state.nextRetryTime).toISOString() : undefined,
        };
    }
    /**
     * Shutdown the circuit breaker and cleanup all resources
     * - Cancels all active timers
     * - Rejects in-flight sleep operations
     * - Prevents new operations from starting
     */
    shutdown() {
        // Already shut down, nothing to do
        if (this.isShuttingDown) {
            return;
        }
        this.isShuttingDown = true;
        // Reject all pending sleep promises
        const shutdownError = new Error('Circuit breaker is shutting down');
        for (const reject of this.pendingRejects) {
            reject(shutdownError);
        }
        this.pendingRejects.clear();
        // Clear all active timers
        for (const timer of this.activeTimers) {
            clearTimeout(timer);
        }
        this.activeTimers.clear();
    }
    /**
     * Check if circuit breaker is shut down
     */
    isShutdown() {
        return this.isShuttingDown;
    }
}
exports.CircuitBreaker = CircuitBreaker;
/**
 * Circuit breaker manager for multiple external dependencies
 */
class CircuitBreakerManager {
    constructor(defaultOptions = {}) {
        this.breakers = new Map();
        this.defaultOptions = defaultOptions;
    }
    /**
     * Get or create circuit breaker for a specific external module
     */
    getBreaker(moduleId, options) {
        if (!this.breakers.has(moduleId)) {
            const breakerOptions = { ...this.defaultOptions, ...options };
            this.breakers.set(moduleId, new CircuitBreaker(breakerOptions));
        }
        return this.breakers.get(moduleId);
    }
    /**
     * Execute operation with circuit breaker protection for specific module
     */
    async execute(moduleId, operation, fallback, options) {
        const breaker = this.getBreaker(moduleId, options);
        return breaker.execute(operation, fallback);
    }
    /**
     * Execute with retry logic for specific module
     */
    async executeWithRetry(moduleId, operation, retryOptions, fallback, breakerOptions) {
        const breaker = this.getBreaker(moduleId, breakerOptions);
        return breaker.executeWithRetry(operation, retryOptions, fallback);
    }
    /**
     * Get status of all circuit breakers
     */
    getAllStatus() {
        const status = {};
        for (const [moduleId, breaker] of this.breakers) {
            status[moduleId] = breaker.getHealthStatus();
        }
        return status;
    }
    /**
     * Reset all circuit breakers
     */
    resetAll() {
        for (const breaker of this.breakers.values()) {
            breaker.reset();
        }
    }
    /**
     * Force all circuit breakers to open state
     */
    forceAllOpen(duration) {
        for (const breaker of this.breakers.values()) {
            breaker.forceOpen(duration);
        }
    }
    /**
     * Remove circuit breaker for specific module
     */
    removeBreaker(moduleId) {
        this.breakers.delete(moduleId);
    }
    /**
     * Get overall health status
     */
    getOverallHealth() {
        let healthyCount = 0;
        let openCount = 0;
        for (const breaker of this.breakers.values()) {
            const status = breaker.getHealthStatus();
            if (status.healthy)
                healthyCount++;
            if (status.state === 'open')
                openCount++;
        }
        return {
            healthy: openCount === 0 && healthyCount === this.breakers.size,
            totalBreakers: this.breakers.size,
            healthyBreakers: healthyCount,
            openBreakers: openCount,
        };
    }
    /**
     * Shutdown all circuit breakers and cleanup resources
     * - Calls shutdown() on all managed circuit breakers
     * - Clears the breaker map
     * - Prevents resource leaks from active timers
     */
    shutdown() {
        for (const breaker of this.breakers.values()) {
            breaker.shutdown();
        }
        this.breakers.clear();
    }
    /**
     * Get count of active timers across all breakers (for monitoring)
     */
    getActiveTimerCount() {
        let count = 0;
        for (const breaker of this.breakers.values()) {
            // @ts-expect-error - accessing private property for monitoring
            count += breaker.activeTimers.size;
        }
        return count;
    }
}
exports.CircuitBreakerManager = CircuitBreakerManager;
//# sourceMappingURL=circuit-breaker.js.map