3.2 KiB
circuit-breaker
(no submodules)
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). beforeRequestthrowsWebWorkersError(CIRCUIT_OPEN)when open and the cool-down hasn't elapsed; transitions to half-open when it has.recordSuccesscloses a half-open breaker and resets the consecutive failure counter.recordFailurere-opens a half-open breaker immediately, or opens a closed breaker once the threshold is reached.- Injectable
Clockfor deterministic tests.
Usage: Basic Example
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
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— throwsWebWorkersError(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.