ai-patterns
Version:
Production-ready TypeScript patterns to build solid and robust AI applications. Retry logic, circuit breakers, rate limiting, human-in-the-loop escalation, prompt versioning, response validation, context window management, and more—all with complete type
245 lines • 8.76 kB
JavaScript
"use strict";
/**
* Circuit Breaker Pattern - Protect against failing external services
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.circuitBreaker = exports.CircuitBreaker = void 0;
exports.defineCircuitBreaker = defineCircuitBreaker;
const common_1 = require("../types/common");
const errors_1 = require("../types/errors");
const circuit_breaker_1 = require("../types/circuit-breaker");
/**
* Circuit Breaker - Protects application from failing services
*/
class CircuitBreaker {
constructor(fn, options = {}) {
this.fn = fn;
this.state = circuit_breaker_1.CircuitState.CLOSED;
this.consecutiveFailures = 0;
this.totalFailures = 0;
this.totalSuccesses = 0;
this.totalCalls = 0;
this.lastFailureTime = null;
this.lastSuccessTime = null;
this.halfOpenAttempts = 0;
this.nextAttemptTime = 0;
this.failureThreshold = options.failureThreshold ?? 5;
this.openDuration = options.openDuration ?? 60000;
this.halfOpenMaxAttempts = options.halfOpenMaxAttempts ?? 1;
this.timeout = options.timeout ?? 30000;
this.shouldCountFailure = options.shouldCountFailure ?? (() => true);
this.logger = options.logger ?? common_1.defaultLogger;
this.onStateChange = options.onStateChange;
this.onOpen = options.onOpen;
this.onClose = options.onClose;
this.onHalfOpen = options.onHalfOpen;
// Validation
if (this.failureThreshold < 1) {
throw new errors_1.PatternError(`failureThreshold must be >= 1, received: ${this.failureThreshold}`, errors_1.ErrorCode.CIRCUIT_INVALID_CONFIG);
}
}
/**
* Execute function with circuit breaker protection
*/
async execute(...args) {
this.totalCalls++;
// Check circuit state
if (this.state === circuit_breaker_1.CircuitState.OPEN) {
// Check if we can transition to half-open
if (Date.now() >= this.nextAttemptTime) {
this.changeState(circuit_breaker_1.CircuitState.HALF_OPEN);
}
else {
const retryInSeconds = Math.ceil((this.nextAttemptTime - Date.now()) / 1000);
throw new errors_1.PatternError(`Circuit is open. Retry in ${retryInSeconds}s`, errors_1.ErrorCode.CIRCUIT_OPEN, undefined, {
state: this.state,
retryInSeconds,
nextAttemptTime: this.nextAttemptTime,
});
}
}
try {
// Execute with timeout
const result = await this.executeWithTimeout(args);
this.onSuccess();
return result;
}
catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.onFailure(err);
throw err;
}
}
/**
* Execute function with timeout
*/
async executeWithTimeout(args) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new errors_1.PatternError(`Timeout after ${this.timeout}ms`, errors_1.ErrorCode.TIMEOUT, undefined, { timeout: this.timeout }));
}, this.timeout);
this.fn(...args)
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}
/**
* Handle successful execution
*/
onSuccess() {
this.totalSuccesses++;
this.lastSuccessTime = Date.now();
if (this.state === circuit_breaker_1.CircuitState.HALF_OPEN) {
this.halfOpenAttempts++;
// If enough successful attempts, close circuit
if (this.halfOpenAttempts >= this.halfOpenMaxAttempts) {
this.logger.info("Circuit closed after successful half-open tests");
this.changeState(circuit_breaker_1.CircuitState.CLOSED);
this.consecutiveFailures = 0;
this.halfOpenAttempts = 0;
}
}
else if (this.state === circuit_breaker_1.CircuitState.CLOSED) {
// Reset consecutive failure count on success
this.consecutiveFailures = 0;
}
}
/**
* Handle failed execution
*/
onFailure(error) {
this.lastFailureTime = Date.now();
// Check if error should count
if (!this.shouldCountFailure(error)) {
this.logger.debug("Failure ignored", { error: error.message });
return;
}
this.consecutiveFailures++;
this.totalFailures++;
if (this.state === circuit_breaker_1.CircuitState.HALF_OPEN) {
// Any failure in half-open reopens circuit
this.logger.warn("Failure in half-open, reopening circuit");
this.changeState(circuit_breaker_1.CircuitState.OPEN);
this.halfOpenAttempts = 0;
this.nextAttemptTime = Date.now() + this.openDuration;
}
else if (this.state === circuit_breaker_1.CircuitState.CLOSED) {
// Check if threshold reached
if (this.consecutiveFailures >= this.failureThreshold) {
this.logger.warn(`Failure threshold reached (${this.consecutiveFailures}/${this.failureThreshold}), opening circuit`);
this.changeState(circuit_breaker_1.CircuitState.OPEN);
this.nextAttemptTime = Date.now() + this.openDuration;
}
}
}
/**
* Change circuit state
*/
changeState(newState) {
const oldState = this.state;
if (oldState === newState)
return;
this.state = newState;
this.logger.info(`Circuit: ${oldState} → ${newState}`);
if (this.onStateChange) {
this.onStateChange(oldState, newState);
}
switch (newState) {
case circuit_breaker_1.CircuitState.OPEN:
if (this.onOpen)
this.onOpen();
break;
case circuit_breaker_1.CircuitState.CLOSED:
if (this.onClose)
this.onClose();
break;
case circuit_breaker_1.CircuitState.HALF_OPEN:
if (this.onHalfOpen)
this.onHalfOpen();
break;
}
}
/**
* Get circuit statistics
*/
getStats() {
return {
state: this.state,
failureCount: this.totalFailures,
successCount: this.totalSuccesses,
totalCalls: this.totalCalls,
lastFailureTime: this.lastFailureTime,
lastSuccessTime: this.lastSuccessTime,
};
}
/**
* Reset circuit to closed state
*/
reset() {
this.state = circuit_breaker_1.CircuitState.CLOSED;
this.consecutiveFailures = 0;
this.totalFailures = 0;
this.totalSuccesses = 0;
this.halfOpenAttempts = 0;
this.nextAttemptTime = 0;
this.logger.info("Circuit reset");
}
/**
* Get current state
*/
getState() {
return this.state;
}
}
exports.CircuitBreaker = CircuitBreaker;
/**
* Define a circuit breaker with Vercel-style callable API
*
* @example
* ```typescript
* const breaker = defineCircuitBreaker({
* execute: async () => callAPI(),
* failureThreshold: 5,
* openDuration: 60000
* });
*
* const result = await breaker(); // Direct call
* console.log(breaker.getState()); // Check state
* ```
*/
function defineCircuitBreaker(options) {
const { execute: fn, failureThreshold, openDuration, halfOpenMaxAttempts, timeout, shouldCountFailure, logger, onStateChange, onOpen, onClose, onHalfOpen, } = options;
const instance = new CircuitBreaker(fn, {
failureThreshold,
openDuration,
halfOpenMaxAttempts,
timeout,
shouldCountFailure,
logger,
onStateChange,
onOpen,
onClose,
onHalfOpen,
});
// Create callable function (Vercel-style)
const callable = async (...args) => {
return await instance.execute(...args);
};
// Attach utility methods
callable.getState = () => instance.getState();
callable.getStats = () => instance.getStats();
callable.reset = () => instance.reset();
return callable;
}
/**
* @deprecated Use `defineCircuitBreaker` instead for better alignment with Vercel AI SDK patterns
* @see defineCircuitBreaker
*/
exports.circuitBreaker = defineCircuitBreaker;
//# sourceMappingURL=circuit-breaker.js.map