UNPKG

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