UNPKG

@allan1361/iota-big3-sdk-middleware

Version:

🏆 A+ Grade Certified Enterprise Middleware Framework - Phase 3 Certified (90/100) with advanced resilience patterns, comprehensive type safety, and production-ready observability

295 lines 10.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CircuitBreaker = void 0; exports.createCircuitBreaker = createCircuitBreaker; const events_1 = require("events"); class CircuitBreaker extends events_1.EventEmitter { constructor(config = {}) { super(); this.state = 'CLOSED'; this.requestHistory = []; this.nextAttempt = 0; this.config = { failureThreshold: config.failureThreshold || 5, successThreshold: config.successThreshold || 2, timeWindow: config.timeWindow || 60000, resetTimeout: config.resetTimeout || 30000, requestTimeout: config.requestTimeout || 3000, errorThresholdPercentage: config.errorThresholdPercentage || 50, volumeThreshold: config.volumeThreshold || 10, isFailure: config.isFailure || this.defaultIsFailure, fallback: config.fallback || this.defaultFallback, onOpen: config.onOpen || (() => { }), onClose: config.onClose || (() => { }), onHalfOpen: config.onHalfOpen || (() => { }), healthCheck: config.healthCheck, healthCheckInterval: config.healthCheckInterval || 5000 }; this.metrics = this.initializeMetrics(); if (this.config.healthCheck) { this.startHealthCheck(); } } middleware(routeName) { return async (req, res, next) => { if (this.state === 'OPEN') { const now = Date.now(); if (now >= this.nextAttempt) { this.transitionTo('HALF_OPEN'); } else { this.emit('rejected', { req, state: this.state }); return this.config.fallback(req, res); } } const startTime = Date.now(); let timedOut = false; let requestCompleted = false; const timeout = setTimeout(() => { if (!requestCompleted) { timedOut = true; this.recordFailure(new Error('Request timeout'), startTime); if (!res.headersSent) { res.status(504).json({ error: 'Circuit breaker timeout', circuit: routeName || 'default', state: this.state }); } } }, this.config.requestTimeout); const originalSend = res.send; const originalJson = res.json; const originalEnd = res.end; const completeRequest = (error) => { if (!requestCompleted && !timedOut) { requestCompleted = true; clearTimeout(timeout); if (error || this.config.isFailure(error || null, res)) { this.recordFailure(error || new Error('Request failed'), startTime); } else { this.recordSuccess(startTime); } } }; res.send = function (body) { completeRequest(); return originalSend.call(this, body); }; res.json = function (body) { completeRequest(); return originalJson.call(this, body); }; res.end = function (chunk, encoding) { completeRequest(); return originalEnd.call(this, chunk, encoding); }; const errorHandler = (error) => { completeRequest(error); next(error); }; try { next(); } catch (error) { errorHandler(error); } }; } async execute(fn) { if (this.state === 'OPEN') { const now = Date.now(); if (now >= this.nextAttempt) { this.transitionTo('HALF_OPEN'); } else { throw new Error(`Circuit breaker is OPEN`); } } const startTime = Date.now(); try { const result = await Promise.race([ fn(), new Promise((_, reject) => setTimeout(() => reject(new Error('Circuit breaker timeout')), this.config.requestTimeout)) ]); this.recordSuccess(startTime); return result; } catch (error) { this.recordFailure(error, startTime); throw error; } } getState() { return this.state; } getMetrics() { return { ...this.metrics }; } reset() { this.transitionTo('CLOSED'); this.metrics = this.initializeMetrics(); this.requestHistory = []; this.emit('reset', this.metrics); } open() { this.transitionTo('OPEN'); } close() { this.transitionTo('CLOSED'); } stop() { if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); this.healthCheckTimer = undefined; } } initializeMetrics() { return { failures: 0, successes: 0, totalRequests: 0, errorPercentage: 0, consecutiveFailures: 0, consecutiveSuccesses: 0, state: 'CLOSED', stateChangedAt: new Date() }; } recordSuccess(startTime) { const duration = Date.now() - startTime; this.requestHistory.push({ timestamp: Date.now(), success: true, duration }); this.metrics.successes++; this.metrics.totalRequests++; this.metrics.consecutiveSuccesses++; this.metrics.consecutiveFailures = 0; this.metrics.lastSuccessTime = new Date(); this.cleanupHistory(); this.updateErrorPercentage(); if (this.state === 'HALF_OPEN') { if (this.metrics.consecutiveSuccesses >= this.config.successThreshold) { this.transitionTo('CLOSED'); } } this.emit('success', { duration, state: this.state }); } recordFailure(error, startTime) { const duration = Date.now() - startTime; this.requestHistory.push({ timestamp: Date.now(), success: false, duration, error }); this.metrics.failures++; this.metrics.totalRequests++; this.metrics.consecutiveFailures++; this.metrics.consecutiveSuccesses = 0; this.metrics.lastFailureTime = new Date(); this.cleanupHistory(); this.updateErrorPercentage(); if (this.state === 'CLOSED' || this.state === 'HALF_OPEN') { if (this.shouldOpen()) { this.transitionTo('OPEN'); } } this.emit('failure', { error, duration, state: this.state }); } shouldOpen() { if (this.metrics.consecutiveFailures >= this.config.failureThreshold) { return true; } const recentRequests = this.getRecentRequests(); if (recentRequests.length >= this.config.volumeThreshold) { const failures = recentRequests.filter(r => !r.success).length; const errorPercentage = (failures / recentRequests.length) * 100; if (errorPercentage >= this.config.errorThresholdPercentage) { return true; } } return false; } transitionTo(newState) { if (this.state === newState) return; const oldState = this.state; this.state = newState; this.metrics.state = newState; this.metrics.stateChangedAt = new Date(); switch (newState) { case 'OPEN': this.nextAttempt = Date.now() + this.config.resetTimeout; this.config.onOpen(this.metrics); break; case 'CLOSED': this.metrics.consecutiveFailures = 0; this.config.onClose(this.metrics); break; case 'HALF_OPEN': this.metrics.consecutiveSuccesses = 0; this.metrics.consecutiveFailures = 0; this.config.onHalfOpen(this.metrics); break; } this.emit('stateChange', { from: oldState, to: newState, metrics: this.metrics }); } cleanupHistory() { const cutoff = Date.now() - this.config.timeWindow; this.requestHistory = this.requestHistory.filter(r => r.timestamp > cutoff); } getRecentRequests() { const cutoff = Date.now() - this.config.timeWindow; return this.requestHistory.filter(r => r.timestamp > cutoff); } updateErrorPercentage() { const recent = this.getRecentRequests(); if (recent.length === 0) { this.metrics.errorPercentage = 0; } else { const failures = recent.filter(r => !r.success).length; this.metrics.errorPercentage = (failures / recent.length) * 100; } } defaultIsFailure(error, response) { if (error) return true; if (response && response.statusCode >= 500) return true; return false; } defaultFallback(req, res) { res.status(503).json({ error: 'Service temporarily unavailable', message: 'Circuit breaker is OPEN', retryAfter: Math.max(0, this.nextAttempt - Date.now()) }); } startHealthCheck() { if (!this.config.healthCheck) return; this.healthCheckTimer = setInterval(async () => { if (this.state === 'OPEN' && Date.now() >= this.nextAttempt) { try { const healthy = await this.config.healthCheck(); if (healthy) { this.transitionTo('HALF_OPEN'); } } catch (error) { this.nextAttempt = Date.now() + this.config.resetTimeout; } } }, this.config.healthCheckInterval); } } exports.CircuitBreaker = CircuitBreaker; function createCircuitBreaker(config) { return new CircuitBreaker(config); } //# sourceMappingURL=circuit-breaker-middleware.js.map