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,94 @@
# `circuit-breaker`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`circuit-breaker.ts` exposes `CircuitBreaker`, a per-worker failure-
isolating state machine the base manager uses to short-circuit
`sendRequest` calls to workers that have failed `failureThreshold`
consecutive times. The breaker transitions `closed → open` after the
failure budget is exhausted, `open → half-open` once `resetMs` has
elapsed, and either `closed` (on success) or back to `open` (on failure)
from half-open.
Adding a breaker bounds the blast radius of a flaky worker: instead of
every request waiting for the full timeout, the breaker rejects them
synchronously with `WEB_WORKERS.CIRCUIT_OPEN` and lets the cool-down do
its job. The base manager calls `beforeRequest` / `recordSuccess` /
`recordFailure` at the right inflection points so consumers get the
behaviour for free by adding `circuitBreaker: { ... }` to their
`WorkerConfig`.
## Features
- Three-state state machine: `closed`, `open`, `half-open`.
- Validates configuration (`failureThreshold > 0`, `resetMs >= 0`).
- `beforeRequest` throws `WebWorkersError(CIRCUIT_OPEN)` when open and
the cool-down hasn't elapsed; transitions to half-open when it has.
- `recordSuccess` closes a half-open breaker and resets the consecutive
failure counter.
- `recordFailure` re-opens a half-open breaker immediately, or opens a
closed breaker once the threshold is reached.
- Injectable `Clock` for deterministic tests.
## Usage: Basic Example
```ts
import { WorkerManager } from "@suaveplan/web-workers";
const manager = new WorkerManager({ factory });
manager.create({
name: "flaky",
url: WORKER_URL,
circuitBreaker: { failureThreshold: 3, resetMs: 30_000 },
});
```
## Usage: Advanced Example
```ts
import { CircuitBreaker } from "@suaveplan/web-workers";
class GuardedTransport {
private readonly breaker = new CircuitBreaker(
{ failureThreshold: 5, resetMs: 60_000 },
() => Date.now()
);
async call(op: () => Promise<unknown>): Promise<unknown> {
this.breaker.beforeRequest();
try {
const result = await op();
this.breaker.recordSuccess();
return result;
} catch (err) {
this.breaker.recordFailure();
throw err;
}
}
}
```
## API Reference
- `class CircuitBreaker`.
- `constructor(config: CircuitBreakerConfig, clock: Clock)`.
- `get current: CircuitState`.
- `beforeRequest(): void` — throws `WebWorkersError(CIRCUIT_OPEN)` when
open and cool-down not elapsed.
- `recordSuccess(): void`.
- `recordFailure(): void`.
- `type CircuitState = "closed" | "open" | "half-open"`.
## Implementation Notes
The breaker is single-instance per worker — the base manager creates it
in `create()` and holds it on the `ManagedEntry`. Failures only count
when consecutive, so a successful request mid-failure-window resets the
counter. The half-open state allows exactly one probe request through;
the next call to `recordFailure` re-opens the breaker AND resets the
cool-down timer so the next probe is `resetMs` away. `Clock` is injected
(rather than reading `Date.now` directly) so tests can advance time
deterministically without `vi.useFakeTimers()` machinery.