@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
JavaScript
"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