first commit
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# `base-manager`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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:
|
||||
|
||||
```ts
|
||||
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):
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user