first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
# circuit-breaker
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.