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

5.2 KiB

base-manager

(no submodules)

Purpose

base-manager.ts declares BaseWorkerManager<TTransport>, 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<WorkerManagerEventMap> 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:

import { WorkerManager } from "@suaveplan/web-workers";

const manager = new WorkerManager({ factory });
manager.create({ name: "hash", url: WORKER_URL });
const digest = await manager.sendRequest<string, string>(
    "hash",
    "sha256",
    "hello"
);

Usage: Advanced Example

Subclassing for a custom transport (e.g. a MessageChannel-backed in-process worker):

import {
    BaseWorkerManager,
    type BaseManagerOptions,
    type WorkerConfig,
    type WorkerMessage,
} from "@suaveplan/web-workers";

interface ChannelTransport {
    readonly port: MessagePort;
}

class MessageChannelManager extends BaseWorkerManager<ChannelTransport> {
    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<TTransport> — abstract class.
  • events: EventBus<WorkerManagerEventMap> — 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<T>(name, type, payload, options?) — fire-and-forget; validates transfer.
  • sendRequest<TReq, TRes>(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<TTransport> — 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.