# circuit-breaker _(no submodules)_ ## Purpose Provides `CircuitBreaker`, a state-machine implementation of the circuit-breaker pattern for protecting async functions against cascading failures. The circuit starts `closed` (normal operation) and opens after a configurable number of consecutive failures, temporarily blocking calls to give the underlying resource time to recover. After a timeout, the circuit enters `half-open` state to probe whether the resource has recovered, and closes again on sufficient consecutive successes. State-change events are emitted via an internal `EventBus` from `@suaveplan/events`, enabling observability integrations, metrics, and alerting without polling. ## Features - Three states: `closed`, `open`, `half-open` with deterministic transitions - Configurable `failureThreshold`, `successThreshold`, and `timeout` (open → half-open delay) - Optional `halfOpenProbes` limit to bound probe calls in half-open - Clock injection for deterministic testing and wall-time independence - `stateChange` event emitted on every transition via `EventBus` - `reset()` for forced closure (e.g., operator action or integration tests) - `AbortSignal` passed through to wrapped function ## Usage: Basic Example ```typescript import { CircuitBreaker } from "@suaveplan/retry"; const breaker = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, timeout: 30_000, // ms before half-open }); // Use breaker.call() instead of calling fn directly const data = await breaker.call((signal) => fetchData(url, { signal })); ``` ## Usage: Advanced Example ```typescript import { CircuitBreaker, withRetry, createExponentialBackoff } from "@suaveplan/retry"; const breaker = new CircuitBreaker({ failureThreshold: 3, successThreshold: 2, timeout: 10_000, clock: () => performance.now(), }); // Subscribe to state changes for metrics breaker.on("stateChange", ({ from, to }) => { metrics.increment("circuit_breaker.transition", { from, to }); }); // Combine with retry — circuit rejects fast while open const delay = createExponentialBackoff({ initial: 200, max: 5_000 }); const result = await withRetry( (signal) => breaker.call(() => callDownstreamService(signal)), { maxAttempts: 6, delay, shouldRetry: (err) => !(err instanceof CircuitOpenError), }, ); ``` ## API Reference ### `new CircuitBreaker(config)` | Parameter | Type | Description | |-----------|------|-------------| | `config.failureThreshold` | `number` | Consecutive failures to open the circuit. | | `config.successThreshold` | `number` | Consecutive successes in half-open to close. | | `config.timeout` | `number` | Ms before open transitions to half-open. | | `config.halfOpenProbes` | `number?` | Max probes in half-open (default: `successThreshold`). | | `config.clock` | `Clock?` | Time source. Defaults to `Date.now`. | ### `breaker.state` `CircuitState` — one of `"closed"`, `"open"`, `"half-open"`. ### `breaker.on(event, handler)` Subscribe to `"stateChange"` events. Returns an unsubscribe function. ### `breaker.call(fn, signal?)` Execute `fn` through the circuit. Throws `CircuitOpenError` if open. ### `breaker.reset()` Force circuit to `closed` and reset all counters. Emits `stateChange` unless already closed. ## Implementation Notes The `checkHalfOpen` method is called lazily at the start of every `call()` — this avoids background timers and keeps the circuit breaker self-contained. The `openedAt` timestamp is set using the injected `clock` so deterministic tests can advance time by mutating the clock reference rather than using real timers. The `EventBus` is internal and not exposed as a public property to prevent external mutation. Listeners receive typed payloads without requiring any casting.