# `base-manager` _(no submodules)_ ## Purpose `base-manager.ts` declares `BaseWorkerManager`, the abstract class that hosts everything shared between the classic-Worker manager (`WorkerManager`) and the SharedWorker manager (`SharedWorkerManager`). Both concrete classes extend this base and only override `createTransport`, `terminateTransport`, and `postToTransport` — the rest of the lifecycle, RPC state, AbortSignal wiring, transfer-list validation, backpressure ceiling, circuit-breaker integration, OTel emission, and `EventBus` lifecycle events all live here. Centralising this logic is what makes the contract testable in isolation and identical across transports: the same `terminate-rejects-pending` semantics, the same backpressure code, the same OTel counter increments fire regardless of whether the underlying transport is a `Worker` or a SharedWorker port. ## Features - Per-name worker registry with `idle / busy / error / terminated` status. - Pending-request map with timeout + AbortSignal + backpressure ceiling (`maxPendingRequests`). - Per-worker circuit breaker integration (`config.circuitBreaker`). - Transfer-list validation (rejects non-array `transfer` with `INVALID_TRANSFER`). - `EventBus` emission on every lifecycle event. - OTel counter increments on every send / receive / create / terminate. - RPC latency histogram observation on every fulfilled request. - Multi-handler `onError` registry. ## Usage: Basic Example `BaseWorkerManager` is abstract — consumers always work with `WorkerManager` or `SharedWorkerManager` directly. The shared API surface that `BaseWorkerManager` exposes is therefore consumed via the subclasses: ```ts import { WorkerManager } from "@suaveplan/web-workers"; const manager = new WorkerManager({ factory }); manager.create({ name: "hash", url: WORKER_URL }); const digest = await manager.sendRequest( "hash", "sha256", "hello" ); ``` ## Usage: Advanced Example Subclassing for a custom transport (e.g. a `MessageChannel`-backed in-process worker): ```ts import { BaseWorkerManager, type BaseManagerOptions, type WorkerConfig, type WorkerMessage, } from "@suaveplan/web-workers"; interface ChannelTransport { readonly port: MessagePort; } class MessageChannelManager extends BaseWorkerManager { constructor(opts: BaseManagerOptions) { super(opts); } protected override createTransport(config: WorkerConfig): ChannelTransport { const { port1, port2 } = new MessageChannel(); port1.onmessage = (event) => { this.handleMessage( config.name, event.data as WorkerMessage ); }; port1.start(); // hand `port2` to the worker side somehow (e.g. via postMessage) return { port: port1 }; } protected override terminateTransport(t: ChannelTransport): void { t.port.close(); } protected override postToTransport( t: ChannelTransport, message: unknown, transfer: Transferable[] | undefined ): void { if (transfer !== undefined && transfer.length > 0) { t.port.postMessage(message, transfer); } else { t.port.postMessage(message); } } } ``` ## API Reference - `BaseWorkerManager` — abstract class. - `events: EventBus` — lifecycle event bus. - `create(config: WorkerConfig)` — throws `DUPLICATE_WORKER`. - `terminate(name)` — throws `WORKER_NOT_FOUND`; rejects pending with `WORKER_TERMINATED`. - `terminateAll()` — iterates `terminate(name)` for each. - `has(name): boolean`. - `getStatus(name): WorkerStatus` — throws `WORKER_NOT_FOUND`. - `getWorkerNames(): string[]`. - `onError(handler: (name, error) => void): void`. - `postMessage(name, type, payload, options?)` — fire-and-forget; validates transfer. - `sendRequest(name, type, payload, options?)` — request / response with timeout, signal, transfer, backpressure, circuit breaker. - Protected abstract methods: `createTransport`, `terminateTransport`, `postToTransport`. - Protected helpers: `handleMessage`, `handleError`, `getEntry`. - `ManagedEntry` — exported internal entry type for subclass visibility. ## Implementation Notes The constructor is `protected` — only subclasses can instantiate. The internal `PendingEntry` interface stores both the `timer` handle and the `cleanupSignal` closure so both can be torn down in any of the three exit paths (success, abort, terminate, timeout). The `entry.pending.delete(id)` call happens unconditionally in both the timer + abort paths so a single race where the abort fires after the timer body started running still leaves the pending map in a consistent state. `dispatchRequest` is split out of `sendRequest` to keep both methods under the 50-line soft limit and to give `sendRequest` a clean place to do the synchronous pre-flight (backpressure check, pre-aborted signal, transfer validation, breaker probe). The status field flips `busy → idle` only when the pending count hits zero, so concurrent in-flight requests don't prematurely reset the status.