export type CircuitState = "closed" | "open" | "half-open"; export type CircuitEntry = { state: CircuitState; failures: number; openedAt: number; generation: number; }; /** * @typedef {'closed'|'open'|'half-open'} CircuitState * @typedef {{ state: CircuitState, failures: number, openedAt: number, generation: number }} CircuitEntry */ export class CircuitBreaker { /** * @param {object} [options={}] * @param {number} [options.threshold=5] - Failures before opening. * @param {number} [options.resetAfter=60000] - Ms before half-open probe. * @param {((key: string, from: CircuitState, to: CircuitState) => void)} [options.onStateChange] - Callback(key, from, to). * @when you want per-key failure isolation — trip open after N failures, probe half-open after a cooldown — so one failing dependency doesn't cascade * @fails throws CircuitOpenError while open (fail-fast, no downstream call); transitions emit onStateChange and reset on a successful half-open probe. * @example * const cb = new CircuitBreaker({ threshold: 5 }); * await cb.call('api', () => fetch(url)); */ constructor(options?: { threshold?: number | undefined; resetAfter?: number | undefined; onStateChange?: ((key: string, from: CircuitState, to: CircuitState) => void) | undefined; }); threshold: number; resetAfter: number; onStateChange: ((key: string, from: CircuitState, to: CircuitState) => void) | null; /** @type {Map} */ _keys: Map; /** * @param {string} key * @returns {CircuitEntry} */ _getEntry(key: string): CircuitEntry; /** * @param {CircuitEntry} entry * @param {string} key * @param {CircuitState} newState */ _setState(entry: CircuitEntry, key: string, newState: CircuitState): void; /** * Execute fn through the circuit breaker. * @param {function} fn - Async function to call. * @param {string} [key='default'] - Circuit key for per-key isolation. * @returns {Promise<*>} * @throws {CircuitOpenError} When circuit is open. */ call(fn: Function, key?: string): Promise; /** * Get current state for a key. * @param {string} [key='default'] * @returns {'closed'|'open'|'half-open'} */ getState(key?: string): "closed" | "open" | "half-open"; /** * Force reset a key to closed. * @param {string} [key='default'] */ reset(key?: string): void; /** * Wrap a provider so generate() goes through the circuit breaker. Passthrough props (e.g. * `model`, `name`) are preserved so Loop cost accounting — which reads `provider.model` — * keeps working through the wrapper. * @param {{ generate: (...args: any[]) => Promise, [k: string]: any }} provider - Provider with generate(). * @param {string} [key] - Circuit key. * @returns {{ generate: (...args: any[]) => Promise, [k: string]: any }} Wrapped provider. */ wrapProvider(provider: { generate: (...args: any[]) => Promise; [k: string]: any; }, key?: string): { generate: (...args: any[]) => Promise; [k: string]: any; }; }