Files
skills/docs/foundation/data/web-workers/src/circuit-breaker/circuit-breaker.md
2026-05-29 08:20:56 +01:00

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).
  • 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

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 — 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.