@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
250 lines • 8.62 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.circuitRegistry = exports.CircuitBreakerRegistry = exports.CircuitBreaker = exports.CircuitState = void 0;
var CircuitState;
(function (CircuitState) {
CircuitState["CLOSED"] = "CLOSED";
CircuitState["OPEN"] = "OPEN";
CircuitState["HALF_OPEN"] = "HALF_OPEN";
})(CircuitState || (exports.CircuitState = CircuitState = {}));
class CircuitBreaker {
constructor(name, config = {}) {
this.name = name;
this.state = CircuitState.CLOSED;
this.failures = 0;
this.successes = 0;
this.totalCalls = 0;
this.nextAttemptTime = 0;
this.stateChanges = 0;
this.createdAt = Date.now();
this.recentFailures = [];
this.config = {
failureThreshold: 5,
recoveryTimeout: 60000,
successThreshold: 3,
monitoringWindow: 300000,
maxRetryAttempts: 3,
exponentialBackoff: true,
onStateChange: () => { },
onFailure: () => { },
onSuccess: () => { },
...config
};
}
async execute(operation, fallback) {
this.totalCalls++;
if (this.state === CircuitState.OPEN) {
if (Date.now() < this.nextAttemptTime) {
const error = new Error(`Circuit breaker ${this.name} is OPEN. Next attempt at ${new Date(this.nextAttemptTime)}`);
this.config.onFailure(error);
if (fallback) {
return await Promise.resolve(fallback());
}
throw error;
}
else {
this.setState(CircuitState.HALF_OPEN, 'Recovery timeout reached');
}
}
try {
const result = await this.executeWithRetry(operation);
this.onSuccess();
return result;
}
catch (error) {
this.onFailure(error);
if (fallback) {
try {
return await Promise.resolve(fallback());
}
catch (fallbackError) {
throw error;
}
}
throw error;
}
}
async executeWithRetry(operation) {
let lastError;
let baseDelay = 1000;
for (let attempt = 1; attempt <= this.config.maxRetryAttempts; attempt++) {
try {
return await operation();
}
catch (error) {
lastError = error;
if (attempt === this.config.maxRetryAttempts) {
break;
}
const delay = this.config.exponentialBackoff
? baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000
: baseDelay;
await new Promise(resolve => setTimeout(resolve, Math.min(delay, 10000)));
}
}
throw lastError;
}
onSuccess() {
this.successes++;
this.lastSuccessTime = Date.now();
this.config.onSuccess();
if (this.state === CircuitState.HALF_OPEN) {
if (this.successes >= this.config.successThreshold) {
this.setState(CircuitState.CLOSED, 'Success threshold reached in half-open state');
this.resetCounters();
}
}
else if (this.state === CircuitState.CLOSED) {
this.failures = 0;
this.recentFailures = [];
}
}
onFailure(error) {
this.failures++;
this.lastFailureTime = Date.now();
this.config.onFailure(error);
this.recentFailures.push({
timestamp: Date.now(),
error: error.message,
attempts: this.config.maxRetryAttempts
});
const cutoff = Date.now() - this.config.monitoringWindow;
this.recentFailures = this.recentFailures.filter(f => f.timestamp > cutoff);
if (this.state === CircuitState.CLOSED || this.state === CircuitState.HALF_OPEN) {
if (this.recentFailures.length >= this.config.failureThreshold) {
this.setState(CircuitState.OPEN, `Failure threshold reached: ${this.recentFailures.length} failures`);
this.scheduleRecovery();
}
}
}
setState(newState, reason) {
const oldState = this.state;
this.state = newState;
this.stateChanges++;
console.log(`Circuit breaker ${this.name}: ${oldState} -> ${newState} (${reason})`);
this.config.onStateChange(newState, reason);
}
scheduleRecovery() {
if (this.recoveryTimer) {
clearTimeout(this.recoveryTimer);
}
this.nextAttemptTime = Date.now() + this.config.recoveryTimeout;
this.recoveryTimer = setTimeout(() => {
if (this.state === CircuitState.OPEN) {
this.setState(CircuitState.HALF_OPEN, 'Recovery timer expired');
}
}, this.config.recoveryTimeout);
}
resetCounters() {
this.failures = 0;
this.successes = 0;
this.recentFailures = [];
}
open(reason = 'Manual intervention') {
this.setState(CircuitState.OPEN, reason);
this.scheduleRecovery();
}
close(reason = 'Manual intervention') {
this.setState(CircuitState.CLOSED, reason);
this.resetCounters();
if (this.recoveryTimer) {
clearTimeout(this.recoveryTimer);
this.recoveryTimer = undefined;
}
}
getStats() {
const now = Date.now();
const windowStart = now - this.config.monitoringWindow;
const recentFailureCount = this.recentFailures.filter(f => f.timestamp > windowStart).length;
const recentCallCount = Math.max(recentFailureCount + this.successes, 1);
return {
state: this.state,
failures: this.failures,
successes: this.successes,
totalCalls: this.totalCalls,
failureRate: recentFailureCount / recentCallCount,
lastFailureTime: this.lastFailureTime,
lastSuccessTime: this.lastSuccessTime,
uptime: now - this.createdAt,
stateChanges: this.stateChanges
};
}
getFailureHistory() {
return [...this.recentFailures];
}
isHealthy() {
const stats = this.getStats();
return stats.state === CircuitState.CLOSED && stats.failureRate < 0.1;
}
destroy() {
if (this.recoveryTimer) {
clearTimeout(this.recoveryTimer);
this.recoveryTimer = undefined;
}
}
}
exports.CircuitBreaker = CircuitBreaker;
class CircuitBreakerRegistry {
constructor() {
this.breakers = new Map();
}
static getInstance() {
if (!CircuitBreakerRegistry.instance) {
CircuitBreakerRegistry.instance = new CircuitBreakerRegistry();
}
return CircuitBreakerRegistry.instance;
}
getBreaker(name, config) {
if (!this.breakers.has(name)) {
this.breakers.set(name, new CircuitBreaker(name, config));
}
return this.breakers.get(name);
}
removeBreaker(name) {
const breaker = this.breakers.get(name);
if (breaker) {
breaker.destroy();
return this.breakers.delete(name);
}
return false;
}
getAllStats() {
const stats = {};
for (const [name, breaker] of this.breakers.entries()) {
stats[name] = breaker.getStats();
}
return stats;
}
getHealthStatus() {
const healthy = [];
const unhealthy = [];
for (const [name, breaker] of this.breakers.entries()) {
if (breaker.isHealthy()) {
healthy.push(name);
}
else {
unhealthy.push(name);
}
}
return { healthy, unhealthy };
}
emergencyStop() {
for (const [name, breaker] of this.breakers.entries()) {
breaker.open(`Emergency stop triggered`);
}
}
resetAll() {
for (const [name, breaker] of this.breakers.entries()) {
breaker.close(`Global reset triggered`);
}
}
destroy() {
for (const breaker of this.breakers.values()) {
breaker.destroy();
}
this.breakers.clear();
}
}
exports.CircuitBreakerRegistry = CircuitBreakerRegistry;
exports.circuitRegistry = CircuitBreakerRegistry.getInstance();
//# sourceMappingURL=circuitBreaker.js.map