tasq/node_modules/@claude-flow/shared/dist/resilience/circuit-breaker.js

233 lines
6.9 KiB
JavaScript

/**
* Circuit Breaker Pattern
*
* Prevents cascading failures by breaking the circuit after failures.
*
* @module v3/shared/resilience/circuit-breaker
*/
import { EventEmitter } from 'events';
/**
* Circuit breaker states
*/
export var CircuitBreakerState;
(function (CircuitBreakerState) {
/** Circuit is closed, requests flow normally */
CircuitBreakerState["CLOSED"] = "CLOSED";
/** Circuit is open, requests are rejected immediately */
CircuitBreakerState["OPEN"] = "OPEN";
/** Circuit is testing if service recovered */
CircuitBreakerState["HALF_OPEN"] = "HALF_OPEN";
})(CircuitBreakerState || (CircuitBreakerState = {}));
/**
* Default options
*/
const DEFAULT_OPTIONS = {
failureThreshold: 5,
successThreshold: 3,
timeout: 30000,
rollingWindow: 60000,
volumeThreshold: 10,
};
/**
* Circuit Breaker
*
* Implements the circuit breaker pattern to prevent cascading failures.
*
* @example
* const breaker = new CircuitBreaker({
* name: 'external-api',
* failureThreshold: 5,
* timeout: 30000,
* });
*
* try {
* const result = await breaker.execute(() => fetchExternalAPI());
* } catch (error) {
* if (error.message === 'Circuit is open') {
* // Handle circuit open case
* }
* }
*/
export class CircuitBreaker extends EventEmitter {
options;
state = CircuitBreakerState.CLOSED;
requests = [];
halfOpenSuccesses = 0;
openedAt = null;
lastFailure = null;
lastSuccess = null;
rejectedCount = 0;
timeoutId;
constructor(options) {
super();
this.options = { ...DEFAULT_OPTIONS, ...options };
}
/**
* Execute a function through the circuit breaker
*/
async execute(fn) {
// Clean up old requests
this.cleanOldRequests();
// Check if circuit should be tested
this.checkState();
// If open, reject immediately or use fallback
if (this.state === CircuitBreakerState.OPEN) {
this.rejectedCount++;
const error = new Error(`Circuit breaker '${this.options.name}' is open`);
if (this.options.fallback) {
return this.options.fallback(error);
}
throw error;
}
try {
const result = await fn();
this.onSuccess();
return result;
}
catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
// Check if this should be counted as failure
const isFailure = this.options.isFailure?.(err) ?? true;
if (isFailure) {
this.onFailure(err);
}
throw error;
}
}
/**
* Get current state
*/
getState() {
this.checkState();
return this.state;
}
/**
* Get statistics
*/
getStats() {
this.cleanOldRequests();
return {
state: this.state,
failures: this.requests.filter((r) => !r.success).length,
successes: this.requests.filter((r) => r.success).length,
totalRequests: this.requests.length,
rejectedRequests: this.rejectedCount,
lastFailure: this.lastFailure,
lastSuccess: this.lastSuccess,
openSince: this.openedAt,
};
}
/**
* Force reset the circuit breaker
*/
reset() {
const previousState = this.state;
this.state = CircuitBreakerState.CLOSED;
this.requests = [];
this.halfOpenSuccesses = 0;
this.openedAt = null;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
if (previousState !== this.state) {
this.notifyStateChange(previousState, this.state);
}
}
/**
* Handle successful request
*/
onSuccess() {
this.lastSuccess = new Date();
this.requests.push({ timestamp: Date.now(), success: true });
if (this.state === CircuitBreakerState.HALF_OPEN) {
this.halfOpenSuccesses++;
if (this.halfOpenSuccesses >= this.options.successThreshold) {
this.transitionTo(CircuitBreakerState.CLOSED);
this.halfOpenSuccesses = 0;
}
}
}
/**
* Handle failed request
*/
onFailure(error) {
this.lastFailure = new Date();
this.requests.push({ timestamp: Date.now(), success: false });
if (this.state === CircuitBreakerState.HALF_OPEN) {
// Failed during half-open, go back to open
this.transitionTo(CircuitBreakerState.OPEN);
this.halfOpenSuccesses = 0;
return;
}
// Check if we should open the circuit
const failures = this.requests.filter((r) => !r.success).length;
const totalRequests = this.requests.length;
if (totalRequests >= this.options.volumeThreshold &&
failures >= this.options.failureThreshold) {
this.transitionTo(CircuitBreakerState.OPEN);
}
}
/**
* Check if state should change based on timeout
*/
checkState() {
if (this.state === CircuitBreakerState.OPEN && this.openedAt) {
const elapsed = Date.now() - this.openedAt.getTime();
if (elapsed >= this.options.timeout) {
this.transitionTo(CircuitBreakerState.HALF_OPEN);
}
}
}
/**
* Transition to new state
*/
transitionTo(newState) {
const previousState = this.state;
if (previousState === newState) {
return;
}
this.state = newState;
if (newState === CircuitBreakerState.OPEN) {
this.openedAt = new Date();
this.scheduleHalfOpen();
}
else if (newState === CircuitBreakerState.CLOSED) {
this.openedAt = null;
this.requests = [];
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
}
this.notifyStateChange(previousState, newState);
}
/**
* Schedule transition to half-open
*/
scheduleHalfOpen() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.timeoutId = setTimeout(() => {
if (this.state === CircuitBreakerState.OPEN) {
this.transitionTo(CircuitBreakerState.HALF_OPEN);
}
}, this.options.timeout);
}
/**
* Notify state change
*/
notifyStateChange(from, to) {
this.emit('stateChange', { from, to });
this.options.onStateChange?.(from, to);
}
/**
* Clean old requests outside rolling window
*/
cleanOldRequests() {
const cutoff = Date.now() - this.options.rollingWindow;
this.requests = this.requests.filter((r) => r.timestamp >= cutoff);
}
}
//# sourceMappingURL=circuit-breaker.js.map