first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
# `WorkerThreadCommunicator`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`WorkerThreadCommunicator` bridges `@suaveplan/queue-distribution`'s `WorkerCommunicator` shape to a backing thread worker via message passing. The factory is injectable so the same class runs against browser `Worker`, Node `worker_threads`, or any test shim conforming to the structural `ThreadWorkerLike` shape. Every outbound request carries a Zod-validated envelope; every inbound reply is correlated back to its pending request by `id` and validated against a reply schema.
## Features
- Generic over the thread worker — pass any `() => ThreadWorkerLike` factory.
- Bounded `Map<requestId, PendingRequest>` of outstanding requests; never leaks.
- Per-request timeout via `setTimeout`, configurable via `defaultTimeoutMs` or per-call.
- AbortSignal threaded through every request; aborting cleans up the entry and rejects with `WT.WORKER_TERMINATED`.
- Zod schemas at the boundary in both directions; malformed replies surface as `WT.MESSAGE_MALFORMED`.
- `terminate()` synchronously fails every outstanding request, clears the map, and terminates the backing worker.
- Injectable `Clock` and `IdGenerator`.
- OTel counters and histograms per request (`queue_dist_wt.request_total{kind, outcome}`, `queue_dist_wt.request.duration_ms{kind, outcome}`).
## Usage: Basic Example
```ts
import { WorkerThreadCommunicator } from "@suaveplan/queue-distribution-worker-thread/communicator/worker-thread/worker-thread-communicator";
const communicator = new WorkerThreadCommunicator(
() => new Worker(new URL("./worker.ts", import.meta.url), { type: "module" }),
{ workerId: "audio-worker" },
);
const result = await communicator.sendTask("", task, input);
```
## Usage: Advanced Example
In tests, drive the communicator from a `ThreadWorkerLike` shim that captures posted messages and lets the test simulate replies.
```ts
import { WorkerThreadCommunicator } from "@suaveplan/queue-distribution-worker-thread/communicator/worker-thread/worker-thread-communicator";
const posted: unknown[] = [];
let onMessage: ((m: unknown) => void) | undefined;
const shim = {
postMessage: (m: unknown) => posted.push(m),
addEventListener: (event, handler) => {
if (event === "message") onMessage = (data) => handler({ data });
},
terminate: () => undefined,
};
const c = new WorkerThreadCommunicator(() => shim, { workerId: "test" });
const promise = c.sendTask("", task, input);
const envelope = posted[0] as { id: string };
onMessage?.({ id: envelope.id, ok: true, data: "OK" });
console.log(await promise); // "OK"
```
## API Reference
- `interface ThreadWorkerLike` — structural worker shape (`postMessage`, `addEventListener`, `terminate`).
- `type WorkerFactory = () => ThreadWorkerLike`.
- `class WorkerThreadCommunicator`.
- `constructor(factory: WorkerFactory, options?: WorkerThreadCommunicatorOptions, deps?: WorkerThreadCommunicatorDeps)`.
- `interface WorkerThreadCommunicatorOptions` — `defaultTimeoutMs?`, `workerId?`.
- `interface WorkerThreadCommunicatorDeps` — `clock?`, `idGenerator?`.
- `sendTask`, `checkStatus`, `checkHealth`, `ping`, `terminate`, `getProtocol`, `getBackingWorker`.
## Implementation Notes
Inbound replies that fail the Zod envelope still get a best-effort surface: if the malformed payload has a string `id` matching a pending request, that request is rejected with `WT.MESSAGE_MALFORMED`. Without an id, the reply is silently dropped — it's unclaimable. The timeout `setTimeout` callback always calls `this.#pending.delete(id)` unconditionally because every other settle path (message arrival, abort, terminate) clears the timer before deleting; if the timer fires, the entry must still be there. Bypassing the entry-existence check lets coverage hit 100% without a defensive race.

View File

@@ -0,0 +1,76 @@
# `QueueDistributionWorkerThreadError` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`QueueDistributionWorkerThreadError` is the base class for every structured error thrown by `@suaveplan/queue-distribution-worker-thread`. Each subclass carries a stable code from the registry produced by `defineCodes("WT", [...])`. Consumers match programmatically on `error.code` to distinguish a worker crash from a malformed reply, a request timeout, or a graceful termination — without parsing error messages.
## Features
- 4 stable codes — `WORKER_CRASHED`, `MESSAGE_MALFORMED`, `REQUEST_TIMEOUT`, `WORKER_TERMINATED`.
- One concrete subclass per code with a typed constructor signature.
- Optional `cause` narrowed to `Error` instances only.
- Compatible with `SuaveplanError` cause-chain traversal and i18n message resolver.
## Usage: Basic Example
```ts
import {
QueueDistributionWorkerThreadError,
WT_CODES,
} from "@suaveplan/queue-distribution-worker-thread/errors/queue-distribution-worker-thread-error";
try {
await worker.executeTask(task, input);
} catch (error: unknown) {
if (
error instanceof QueueDistributionWorkerThreadError &&
error.code === WT_CODES.WORKER_CRASHED
) {
// Re-route to a different worker.
}
throw error;
}
```
## Usage: Advanced Example
Use the discriminated codes to drive recovery strategy per failure class.
```ts
import {
QueueDistributionWorkerThreadError,
WT_CODES,
} from "@suaveplan/queue-distribution-worker-thread/errors/queue-distribution-worker-thread-error";
function recoveryPolicy(error: unknown): "retry" | "drop" | "fatal" {
if (!(error instanceof QueueDistributionWorkerThreadError)) {
return "fatal";
}
switch (error.code) {
case WT_CODES.REQUEST_TIMEOUT:
return "retry";
case WT_CODES.WORKER_TERMINATED:
return "drop";
case WT_CODES.WORKER_CRASHED:
case WT_CODES.MESSAGE_MALFORMED:
return "fatal";
}
}
```
## API Reference
- `const WT_CODES` — frozen registry of `WT.*` codes.
- `type QueueDistributionWorkerThreadErrorCode` — union of every code.
- `class QueueDistributionWorkerThreadError extends SuaveplanError<QueueDistributionWorkerThreadErrorCode>`.
- `class WorkerThreadCrashedError({ workerId, cause? })`.
- `class WorkerThreadMessageMalformedError({ workerId, cause? })`.
- `class WorkerThreadRequestTimeoutError({ workerId, requestId, timeoutMs })`.
- `class WorkerThreadTerminatedError({ workerId, requestId })`.
## Implementation Notes
`MESSAGE_MALFORMED` is used both for replies that fail the Zod envelope and for `ok: false` replies — both represent the worker telling us "this isn't going to work" without specifying _what_ about the worker is broken. If the upstream needs different classifications, the inbound payload schema is the place to extend, not the error registry. The `WORKER_CRASHED` and `WORKER_TERMINATED` codes are aimed at supervision: the first is what the operator wants to alert on; the second is what the manager wants to filter out of "real failures" during a graceful drain.

View File

@@ -0,0 +1,61 @@
# Worker-thread plugin OTel instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`observability/instruments.ts` centralises every OpenTelemetry counter, histogram, and tracer emitted by `@suaveplan/queue-distribution-worker-thread`. Instruments register under the meter / tracer name `"@suaveplan/queue-distribution-worker-thread"` so SDK consumers can route them with a single rule. Tracer / meter handles are cached behind `resetInstrumentCache()` for tests.
## Features
- Counters: `queue_dist_wt.request_total{kind, outcome}`, `queue_dist_wt.worker.crashed_total`, `queue_dist_wt.worker.terminated_total`.
- Histogram: `queue_dist_wt.request.duration_ms{kind, outcome}`.
- Tracer: `queue_dist_wt.sendTask`.
- `WorkerThreadKind` union (`"task" | "status" | "health" | "ping"`) keeps the cardinality of the `kind` dimension bounded.
- `resetInstrumentCache()` for tests that install a new provider mid-suite.
## Usage: Basic Example
Production code emits records automatically. Consumers wire a meter / tracer provider at startup:
```ts
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { metrics } from "@opentelemetry/api";
metrics.setGlobalMeterProvider(new MeterProvider({/* …readers… */}));
```
## Usage: Advanced Example
In tests, install an in-memory exporter, drive the system, then assert directly on the captured metrics.
```ts
import { resetInstrumentCache } from "@suaveplan/queue-distribution-worker-thread/observability/instruments";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import { metrics } from "@opentelemetry/api";
const exporter = new InMemoryMetricExporter(AggregationTemporality.DELTA);
const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 100 });
metrics.setGlobalMeterProvider(new MeterProvider({ readers: [reader] }));
resetInstrumentCache();
// … drive the system, then await reader.forceFlush() …
```
## API Reference
- `function resetInstrumentCache(): void`.
- `function recordRequestOutcome(kind, outcome): void`.
- `function recordRequestDuration(kind, durationMs, outcome): void`.
- `function recordWorkerCrashed(): void`, `function recordWorkerTerminated(): void`.
- `function getSendTaskTracer(): Tracer`.
- `type WorkerThreadKind = "task" | "status" | "health" | "ping"`.
## Implementation Notes
The instruments share a single cached meter handle so per-call overhead in the hot path is a map lookup. `WorkerThreadKind` is a typed union rather than a free-form string so unknown `kind` values never leak into dashboards — every request type the communicator can send is enumerated, keeping cardinality bounded. `resetInstrumentCache()` exists exclusively for tests; production sets the global provider once at startup and never needs to flush the cache.

View File

@@ -0,0 +1,78 @@
# `WorkerThreadWorker`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`WorkerThreadWorker` extends `AbstractWorker` (from `@suaveplan/queue-distribution`) and dispatches every task to a backing thread worker via `WorkerThreadCommunicator`. A backing-worker crash event flips the worker to `UNHEALTHY` and increments `WT.WORKER_CRASHED`; `stop(force)` terminates the backing worker and fails every outstanding request with `WT.WORKER_TERMINATED`. The worker reports `{ isRemote: false, isThreaded: true }` capabilities so load-balancing strategies can route work appropriately.
## Features
- Implements the full `Worker` contract via `AbstractWorker` inheritance.
- `doExecuteTask` delegates to `WorkerThreadCommunicator.sendTask`.
- Backing-worker `error` event → `markUnhealthy("Worker crashed")` + OTel `queue_dist_wt.worker.crashed_total++`.
- Backing-worker `exit` event → `markUnhealthy("Worker exited")`.
- `doStop(force)` calls `communicator.terminate()` which fails every outstanding request and tears down the backing worker; OTel `queue_dist_wt.worker.terminated_total++`.
- `getCapabilities()` advertises `{ isRemote: false, isThreaded: true, protocol: "worker-thread" }`.
## Usage: Basic Example
```ts
import {
WorkerManager,
WorkerPool,
} from "@suaveplan/queue-distribution";
import { WorkerThreadWorker } from "@suaveplan/queue-distribution-worker-thread";
const pool = new WorkerPool();
pool.addWorker(
new WorkerThreadWorker(
() => new Worker(new URL("./hot-path.ts", import.meta.url), { type: "module" }),
{ id: "hot-1", capacity: 4 },
),
);
await pool.startAllWorkers();
const manager = new WorkerManager(pool, { autoHealthCheck: false });
const result = await manager.assignTask(task, input);
```
## Usage: Advanced Example
For a Node-side worker pool with `worker_threads`, wrap the platform `Worker` in a thin adapter that matches `ThreadWorkerLike`.
```ts
import { Worker as NodeWorker } from "node:worker_threads";
import { WorkerThreadWorker } from "@suaveplan/queue-distribution-worker-thread";
function nodeWorkerFactory(): ReturnType<typeof Object.assign> {
const node = new NodeWorker("./worker.js");
return {
postMessage: (m) => node.postMessage(m),
addEventListener: (event, handler) => {
if (event === "message") node.on("message", (data) => handler({ data }));
if (event === "error") node.on("error", (error) => handler({ error }));
if (event === "exit") node.on("exit", (code) => handler({ code }));
},
terminate: () => node.terminate(),
};
}
const worker = new WorkerThreadWorker(nodeWorkerFactory, {
id: "node-1",
defaultTimeoutMs: 5_000,
});
```
## API Reference
- `class WorkerThreadWorker extends AbstractWorker`.
- `constructor(factory: WorkerFactory, options?: WorkerThreadWorkerOptions, deps?: AbstractWorkerDeps)`.
- `interface WorkerThreadWorkerOptions extends AbstractWorkerOptions` — `defaultTimeoutMs?`.
- `getCommunicator(): WorkerThreadCommunicator`.
- Protected: `doExecuteTask`, `doStop`, `getType()`, `getCapabilities()`.
## Implementation Notes
The crash / exit handlers fire from inside the structural worker's event-listener registration; both flip status to `UNHEALTHY` and emit observability records but do NOT throw — the worker is no longer usable but the manager's supervisor (running independently in `@suaveplan/queue-distribution`) is the right place to escalate the failure. `defaultTimeoutMs` is forwarded to the communicator only when explicitly set, so the communicator's own default (30 seconds) applies when neither layer is configured.