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,72 @@
# @suaveplan/api-worker-host - Worker-side runtime that hosts exactly one Suaveplan API plugin inside an isolated Worker thread. Provides the bootstrap entry point that handshakes with the main host, wires capability proxies via @suaveplan/api-worker-rpc, dispatches lifecycle commands (install/start/stop/uninstall/reload) to plugin hooks, and surfaces uncaught exceptions as WORKER_HOST.CRASHED envelopes so the main host's LifecycleDriver can transition the plugin to the crashed state.
Worker-side runtime that hosts exactly one Suaveplan API plugin inside
an isolated `Worker` thread. The main-process counterpart is
`@suaveplan/api-host`; this package owns everything that runs *inside*
the worker: bootstrap handshake, capability proxy wiring, lifecycle
command dispatch, and uncaught-exception → `WORKER_HOST.CRASHED`
propagation.
This is API plugin system (design doc §2.2, §5.2a).
The transport layer (`@suaveplan/api-worker-rpc`) is consumed here for
every cross-boundary frame.
<!-- begin:auto-docs -->
## Documentation tree
- [bootstrap](./src/bootstrap/bootstrap.md) — `bootstrapWorkerPlugin` is the single entry point a worker thread invokes to host exactly one Suaveplan API plugin. It…
- [bootstrap](./src/bootstrap/bootstrap.md) — `bootstrapWorkerPlugin` is the single entry point a worker thread invokes to host exactly one Suaveplan API plugin. It…
- [capability-proxies](./src/capability-proxies/capability-proxies.md) — This module turns the capability-handle descriptor table delivered by the main host into a live, frozen map of RPC-back…
- [capability-proxies](./src/capability-proxies/capability-proxies.md) — This module turns the capability-handle descriptor table delivered by the main host into a live, frozen map of RPC-back…
- [crash-detection](./src/crash-detection/crash-detection.md) — This module installs the worker-side safety net that turns silent process death into observable crash telemetry. Withou…
- [crash-detection](./src/crash-detection/crash-detection.md) — This module installs the worker-side safety net that turns silent process death into observable crash telemetry. Withou…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the namespaced `WORKER_HOST.*` error registry consumed by the worker-side runtime that hosts exactly o…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the namespaced `WORKER_HOST.*` error registry consumed by the worker-side runtime that hosts exactly o…
- [lifecycle](./src/lifecycle/lifecycle.md) — This module hosts the worker-side counterpart to the main host's `LifecycleDriver`. It owns the worker's nine-state lif…
- [lifecycle](./src/lifecycle/lifecycle.md) — This module hosts the worker-side counterpart to the main host's `LifecycleDriver`. It owns the worker's nine-state lif…
- [protocol-extension](./src/protocol-extension/protocol-extension.md) — This module defines the Zod schemas that the worker host layers on top of the transport-level frame protocol owned by `…
- [protocol-extension](./src/protocol-extension/protocol-extension.md) — This module defines the Zod schemas that the worker host layers on top of the transport-level frame protocol owned by `…
- [testing](./src/testing/testing.md) — `runWorkerHostConformance` spins up an in-process worker-shaped peer using paired `MessageChannel` ports. It exists so…
- [testing](./src/testing/testing.md) — `runWorkerHostConformance` spins up an in-process worker-shaped peer using paired `MessageChannel` ports. It exists so…
<!-- end:auto-docs -->
## Layout
- [bootstrap](./src/bootstrap/bootstrap.md) — `bootstrapWorkerPlugin`
validates the manifest + capability handles, builds the proxy map,
mounts the lifecycle RPC server, posts the `worker-ready` frame.
- [capability-proxies](./src/capability-proxies/capability-proxies.md) —
`installCapabilityProxies` builds frozen RPC-backed capability slots;
`getCapability` enforces declared capabilities.
- [lifecycle](./src/lifecycle/lifecycle.md) — `createLifecycleReceiver`
owns the nine-state worker-side FSM and the `LifecycleAck` envelope.
- [crash-detection](./src/crash-detection/crash-detection.md) —
`installCrashDetector` wires `uncaughtException` /
`unhandledRejection` to a `WORKER_HOST.CRASHED` envelope before
exiting.
- [protocol-extension](./src/protocol-extension/protocol-extension.md) —
Zod schemas for every worker-host envelope.
- [error-codes](./src/error-codes/error-codes.md) — nine stable
`WORKER_HOST.*` codes.
- [testing](./src/testing/testing.md) —
`runWorkerHostConformance` in-process simulator.
## Stability
- **Version**: `0.1.0` (Original).
- **Tier**: server (requires Node `process` + `MessageChannel`; Node
18+).
- **Dependencies**: `@suaveplan/api-types`,
`@suaveplan/api-worker-rpc`, `@suaveplan/api-lifecycle`,
`@suaveplan/error`, `@suaveplan/events`, `@suaveplan/logger`,
`tslib`, `zod`.
## Verification
```bash
bunx turbo lint typecheck test build --filter=@suaveplan/api-worker-host
bunx turbo test:coverage --filter=@suaveplan/api-worker-host
```
All four coverage dimensions land at 100/100/100/100.

View File

@@ -0,0 +1,100 @@
# Worker bootstrap
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`bootstrapWorkerPlugin` is the single entry point a worker thread
invokes to host exactly one Suaveplan API plugin. It validates the
manifest envelope and capability descriptor table at the wire
boundary, builds the capability proxy map from a freshly-created
`WorkerRpcClient`, instantiates the lifecycle receiver wrapping the
plugin's hooks, mounts a `serveWorkerRpc` handler set on the control
port that exposes `lifecycle.dispatch`, `host.manifest` and
`host.state`, then posts the `worker-ready` frame back so the main
host knows the worker is live. Any wiring failure surfaces as a
typed `WorkerHostError` carrying `WORKER_HOST.MANIFEST_INVALID` or
`WORKER_HOST.BOOTSTRAP_FAILED` so the main host's
`LifecycleDriver` can transition the plugin to `install_failed`.
## Features
- Pure function — no module-load side effects, no global mutation,
no implicit import of `worker_threads`.
- Two ports — `controlPort` for lifecycle, `capabilityPort` for
capability RPC; transports are abstracted through `PortLike` so
the same code paths run under real `MessageChannel` and Node's
`worker_threads` `MessagePort`.
- Returns a `WorkerHost` handle exposing the validated manifest,
the proxy map, the lifecycle receiver and a `close()` disposer.
- Optional `onReady` callback fires immediately after the ready
frame is posted — useful for in-process testing harnesses.
## Usage: Basic
```ts
import {
bootstrapWorkerPlugin,
} from "@suaveplan/api-worker-host/bootstrap";
const host = bootstrapWorkerPlugin({
manifest: {
name: "demo",
version: "0.1.0",
capabilities: ["db"],
tier: "isolated",
},
capabilityHandles: { db: { methods: ["query"] } },
hooks: { install: async () => {} },
controlPort: portFromMainHost,
capabilityPort: capabilityPortFromMainHost,
});
```
## Usage: Advanced
Compose the bootstrap with a real `MessageChannel`-based simulator
for unit tests:
```ts
import { createInProcessPortPair } from "@suaveplan/api-worker-rpc";
import {
bootstrapWorkerPlugin,
} from "@suaveplan/api-worker-host/bootstrap";
const control = createInProcessPortPair();
const caps = createInProcessPortPair();
const host = bootstrapWorkerPlugin({
manifest: { name: "x", version: "0.1.0", capabilities: [], tier: "trusted" },
capabilityHandles: {},
hooks: {},
controlPort: control.serverPort,
capabilityPort: caps.serverPort,
onReady: (frame) => console.log("ready:", frame.manifest.name),
});
host.close();
```
## API Reference
- `bootstrapWorkerPlugin(spec: BootstrapSpec): WorkerHost`
- `BootstrapSpec`:
- `manifest: ManifestEnvelope`
- `capabilityHandles: CapabilityHandles`
- `hooks: PluginLifecycleHooks`
- `controlPort: PortLike` / `capabilityPort: PortLike`
- `onReady?: (frame: WorkerReadyFrame) => void`
- `WorkerHost`: `manifest`, `capabilities`, `receiver`, `close()`
## Implementation Notes
The two ports are intentionally separated so capability RPC cannot
starve lifecycle control (or vice versa) — different queues,
different correlation-id namespaces, different backpressure budgets.
On failure the bootstrap closes any RPC client it already opened
before throwing, so the worker never leaks a half-attached
`MessagePort` listener. The ready frame is posted *after* every
piece of wiring succeeds; an early failure means no ready frame and
the main host can detect a stillborn worker via timeout.

View File

@@ -0,0 +1,82 @@
# Capability proxies (worker side)
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module turns the capability-handle descriptor table delivered by
the main host into a live, frozen map of RPC-backed capability slots
that the worker host hands to the plugin via its `PluginContext`. Every
slot method, regardless of how it looked on the trusted-tier surface,
becomes an async function whose body is a single `client.call(...)`
into `@suaveplan/api-worker-rpc`. The plugin author cannot — by design
— tell whether the capability they hold is in-process or proxied across
the worker boundary; the structural shape is identical.
## Features
- `installCapabilityProxies(handles, client)` — validates the handle
table with Zod, then builds a frozen
`Record<capability, Record<method, fn>>` table; each method dispatches
via `client.call("<capability>.<method>", args)`.
- `getCapability(table, name)` — explicit accessor that raises
`WORKER_HOST.CAPABILITY_HANDLE_MISSING` when the plugin asks for a
capability the main host never declared, so missing wiring fails
loudly rather than producing `undefined` at runtime.
- Slot objects are `Object.create(null)` + `Object.freeze` to deny
accidental mutation and prototype-pollution surface.
## Usage: Basic
```ts
import { createWorkerRpcClient } from "@suaveplan/api-worker-rpc";
import {
installCapabilityProxies,
} from "@suaveplan/api-worker-host/capability-proxies";
const client = createWorkerRpcClient(controlPort);
const caps = installCapabilityProxies(
{ db: { methods: ["query"] } },
client.client
);
await caps.db?.query?.("SELECT 1");
```
## Usage: Advanced
Use `getCapability` inside the plugin context builder so unwired
capabilities never reach handler code:
```ts
import {
getCapability,
installCapabilityProxies,
} from "@suaveplan/api-worker-host/capability-proxies";
const proxies = installCapabilityProxies(handles, client);
const ctx = {
capabilities: {
db: getCapability(proxies, "db"),
logger: getCapability(proxies, "logger"),
},
};
```
## API Reference
- `installCapabilityProxies(handles: CapabilityHandles, client: WorkerRpcClient): CapabilityProxyMap`
- `getCapability(proxies: CapabilityProxyMap, name: string): CapabilityProxy`
- `CapabilityProxy` — `Record<string, (...args) => Promise<unknown>>`
- `CapabilityProxyMap` — `Readonly<Record<string, CapabilityProxy>>`
## Implementation Notes
The wire method name is `"<capability>.<method>"` rather than just
`method` so the main host's RPC server can route each call to the
correct capability adapter without keeping per-channel state. Returning
frozen objects also means consumers of the proxy table cannot
accidentally graft synchronous methods onto a slot — which would break
the trusted-vs-isolated structural-equivalence requirement in spec
delta §"Plugin code cannot distinguish trusted from isolated tier".

View File

@@ -0,0 +1,92 @@
# Worker crash detection
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module installs the worker-side safety net that turns silent
process death into observable crash telemetry. Without it, a plugin's
uncaught synchronous throw or unhandled promise rejection would tear
the Node `Worker` down with no signal to the main host's
`LifecycleDriver` — leaving the plugin stuck in `started` until the
control port's `close` event eventually fires (which is too late to
attribute the failure to a specific plugin handler). The detector
captures both `uncaughtException` and `unhandledRejection`, posts a
`WORKER_HOST.CRASHED` envelope over the control port, then invokes the
configured termination hook (`process.exit(1)` by default).
## Features
- `installCrashDetector({ controlPort, process?, onTerminate? })` —
returns a `CrashDetector` with `close()` + `trigger(cause)`.
- Structural `ProcessLike` surface — unit tests substitute a fake
process so the real test runner does not exit.
- `onTerminate` override — tests record the intended exit code
instead of actually exiting.
- `trigger(cause)` lets the lifecycle layer escalate a fatal
bootstrap error to the same code path as an uncaught throw.
- Reports are wrapped in a typed `WorkerHostError(CRASHED, ..., {
cause })` so the original throwable is preserved via the wire
`cause` field after reconstruction on the main host.
## Usage: Basic
```ts
import {
installCrashDetector,
} from "@suaveplan/api-worker-host/crash-detection";
const detector = installCrashDetector({ controlPort });
// later
detector.close();
```
## Usage: Advanced
Inject a fake `process` for deterministic testing — handlers fire
synchronously, exits are recorded:
```ts
import {
installCrashDetector,
} from "@suaveplan/api-worker-host/crash-detection";
let handler: ((err: Error) => void) | undefined;
const fakeProcess = {
on: (_: string, l: (err: Error) => void) => { handler = l; },
off: () => {},
exit: () => {},
};
const exits: number[] = [];
installCrashDetector({
controlPort: port,
process: fakeProcess as never,
onTerminate: (code) => exits.push(code),
});
handler?.(new Error("boom"));
```
## API Reference
- `installCrashDetector(opts: CrashDetectorOptions): CrashDetector`
- `CrashDetectorOptions`:
- `controlPort: PortLike` — required
- `process?: ProcessLike`
- `onTerminate?: (exitCode: number) => void`
- `CrashDetector`:
- `close(): void`
- `trigger(cause: unknown): void`
- `ProcessLike` — `{ on, off, exit }` subset of Node `process`.
## Implementation Notes
`postMessage` is wrapped in a try/catch because the port may already
be closing by the time a crash fires — best effort there, no nested
crash on top of the original. The `close()` flag also guards against
re-entrant invocations triggered by the very crash handlers being
detached. The default `process` lookup is a `globalThis`-based read
so the module never imports `node:process` at module load — keeping
it tree-shake-clean for any future browser-tier consumer that
substitutes its own `ProcessLike`.

View File

@@ -0,0 +1,88 @@
# Worker host error codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the namespaced `WORKER_HOST.*` error registry consumed by
the worker-side runtime that hosts exactly one Suaveplan API plugin. Every
failure path that originates inside the worker host itself — boot failure,
malformed manifest envelope, missing capability handle, lifecycle hook
throw, handshake timeout, uncaught exception — raises a
`WorkerHostError` carrying one of these codes so the main host's
`LifecycleDriver` can branch deterministically on the literal code value
rather than parsing free-form messages.
The registry is deliberately narrow: capability transport failures
surface through `WORKER_RPC.*` (owned by `@suaveplan/api-worker-rpc`),
plugin-author runtime failures surface through whatever code the plugin's
own error registry defines, and lifecycle FSM violations on the main side
surface through `LIFECYCLE.*`. Codes here describe failures that the
worker host alone can detect and report.
## Features
- `defineCodes("WORKER_HOST", [...])` produces a frozen string-literal
registry consumable as `WORKER_HOST_CODES.CRASHED` and friends.
- `WorkerHostError extends SuaveplanError<WorkerHostCode>` — fully
type-narrowed `code` field; carries the same serialise/redact/i18n
surface as every other `@suaveplan/error` subclass.
- Nine stable codes covering bootstrap, manifest validation, capability
wiring, lifecycle dispatch, handshake, crash detection, and FSM
rejection.
## Usage: Basic
```ts
import { WORKER_HOST_CODES, WorkerHostError } from "@suaveplan/api-worker-host/error-codes";
throw new WorkerHostError(
WORKER_HOST_CODES.MANIFEST_INVALID,
"manifest envelope failed schema check"
);
```
## Usage: Advanced
Branch on code values in the main host's lifecycle driver to drive FSM
transitions and crash-restart accounting:
```ts
import { WORKER_HOST_CODES } from "@suaveplan/api-worker-host/error-codes";
function onWorkerEnvelopeError(err: { code: string }): void {
switch (err.code) {
case WORKER_HOST_CODES.CRASHED:
crashBudget.recordCrash();
driver.transition("crashed");
break;
case WORKER_HOST_CODES.LIFECYCLE_HOOK_THREW:
driver.transition("start_failed");
break;
case WORKER_HOST_CODES.MAIN_HANDSHAKE_TIMEOUT:
driver.transition("install_failed");
break;
default:
driver.recordUnknown(err);
}
}
```
## API Reference
- `WORKER_HOST_CODES` — frozen registry keyed by base name, valued by
fully-qualified `"WORKER_HOST.<NAME>"` string.
- `WorkerHostCode` — discriminated union over every code value.
- `WorkerHostError` — `SuaveplanError<WorkerHostCode>` subclass.
## Implementation Notes
The registry list is closed: adding a code requires an OpenSpec change.
Order matches the spec delta authored in
the specification so the
verification trail (`**Verified by:**` lines) lines up with the test
files that exercise each code path. The error class overrides `name` so
`err.name === "WorkerHostError"` survives across the structured-clone
boundary back to the main host.

View File

@@ -0,0 +1,95 @@
# Worker-side lifecycle receiver
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module hosts the worker-side counterpart to the main host's
`LifecycleDriver`. It owns the worker's nine-state lifecycle FSM
(`loaded` → `installing` → `installed` → `starting` → `started` →
`stopping` → `stopped` → `uninstalling` → `uninstalled`), validates
every inbound lifecycle command at the wire boundary with the Zod
schema from `protocol-extension`, dispatches the matched command to
the plugin author's hook (`install` / `start` / `stop` /
`uninstall` / `reload`), and returns a serialisable `LifecycleAck`
envelope the main host's RPC server forwards back to its driver. The
receiver is intentionally synchronous in its state mutation but
asynchronous in its hook invocation — the hook's promise drives the
ack timing.
## Features
- `createLifecycleReceiver({ hooks, initialState? })` — pure factory;
no global state, no module-load side effects.
- Tracks current state with a closure variable; `state()` exposes it
for observability and tests.
- `dispatch(raw)` validates the command, refuses illegal transitions
with `WORKER_HOST.INVALID_LIFECYCLE_TRANSITION`, runs the matched
hook (if defined), and on throw reconstructs the error via
`api-worker-rpc`'s `errorToEnvelope` while rewinding to the
pre-command state for retry.
- Hook throws are wrapped in
`WORKER_HOST.LIFECYCLE_HOOK_THREW` carrying `cause` + `context`
describing which hook triggered.
- Unknown command kinds raise
`WORKER_HOST.UNKNOWN_LIFECYCLE_COMMAND`.
## Usage: Basic
```ts
import {
createLifecycleReceiver,
} from "@suaveplan/api-worker-host/lifecycle";
const receiver = createLifecycleReceiver({
hooks: {
install: async () => { await db.migrate(); },
start: async () => { httpServer.listen(); },
},
});
const ack = await receiver.dispatch({ command: "install" });
if (ack.ok) console.log("installed");
```
## Usage: Advanced
Pair the receiver with `serveWorkerRpc` so the main host can drive
lifecycle over a dedicated `MessagePort`:
```ts
import { serveWorkerRpc } from "@suaveplan/api-worker-rpc";
import {
createLifecycleReceiver,
} from "@suaveplan/api-worker-host/lifecycle";
const receiver = createLifecycleReceiver({ hooks });
serveWorkerRpc(controlPort, {
"lifecycle.dispatch": async (params) =>
receiver.dispatch(params[0]),
});
```
## API Reference
- `createLifecycleReceiver(opts: LifecycleReceiverOptions): LifecycleReceiver`
- `LifecycleReceiverOptions`:
- `hooks: PluginLifecycleHooks`
- `initialState?: WorkerLifecycleState`
- `LifecycleReceiver`:
- `state(): WorkerLifecycleState`
- `dispatch(raw: unknown): Promise<LifecycleAck>`
- `PluginLifecycleHooks` — optional async hooks per command kind.
- `WorkerLifecycleState` — nine-state union.
## Implementation Notes
Legal-from sets are declared at module scope so the transition table
is auditable in one read. The `*ing` state is exposed mid-hook so the
main host can observe an in-flight transition rather than only the
terminal state. On hook throw, the receiver reverts to the *previous*
state (not the *ing* state) so retry attempts are evaluated against
the correct legal-from set. Schema validation runs before any state
mutation — a malformed envelope cannot push the FSM out of
`loaded` accidentally.

View File

@@ -0,0 +1,99 @@
# Worker host protocol extensions
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module defines the Zod schemas that the worker host layers on top
of the transport-level frame protocol owned by
`@suaveplan/api-worker-rpc`. Where the transport package speaks
`request` / `response` / `cancel` / `stream-*` / `hello` / `goodbye`
frames, the worker host carries higher-level *control envelopes* —
manifest advertisement, lifecycle commands, lifecycle acks, crash
reports and capability-handle descriptors — inside those frames'
payloads. Keeping the envelope schemas in one place lets the bootstrap,
lifecycle and crash-detection modules import a single validator and
gives the conformance harness one round-trip surface to test against.
## Features
- `manifestEnvelopeSchema` — the minimum manifest shape the main host
needs for FSM bookkeeping (name, version, capabilities, tier).
- `workerReadyFrameSchema` — initial readiness frame the worker pushes
after the plugin module has been imported and validated.
- `lifecycleCommandSchema` — discriminated by `command:
"install"|"start"|"stop"|"uninstall"|"reload"`.
- `lifecycleAckSchema` — discriminated union over success / failure
acks carrying a reconstructed `ErrorEnvelope` on failure.
- `crashReportSchema` — emitted by the crash detector before the worker
exits non-zero.
- `capabilityHandlesSchema` — descriptor table delivered by the main
host so the worker knows which capability methods to expose as RPC
proxies.
## Usage: Basic
```ts
import {
workerReadyFrameSchema,
} from "@suaveplan/api-worker-host/protocol-extension";
const ready = workerReadyFrameSchema.parse({
kind: "worker-ready",
manifest: {
name: "demo",
version: "0.1.0",
capabilities: ["db", "logger"],
tier: "isolated",
},
});
```
## Usage: Advanced
Validate every payload at the wire boundary in the lifecycle receiver:
```ts
import {
lifecycleAckSchema,
lifecycleCommandSchema,
} from "@suaveplan/api-worker-host/protocol-extension";
function dispatch(raw: unknown): void {
const cmd = lifecycleCommandSchema.parse(raw);
runHook(cmd.command).then(
() => send(lifecycleAckSchema.parse({ ok: true })),
(err) => send(
lifecycleAckSchema.parse({
ok: false,
error: errorToEnvelope(err),
})
)
);
}
```
## API Reference
- `manifestEnvelopeSchema` / `ManifestEnvelope`
- `workerReadyFrameSchema` / `WorkerReadyFrame`
- `lifecycleCommandKindSchema` / `LifecycleCommandKind`
- `lifecycleCommandSchema` / `LifecycleCommand`
- `lifecycleAckOkSchema` / `lifecycleAckErrSchema` / `lifecycleAckSchema`
/ `LifecycleAck`
- `crashReportSchema` / `CrashReport`
- `capabilityHandlesSchema` / `CapabilityHandles`
- `errorEnvelopeSchema` / `ErrorEnvelope` — re-exported from the
transport layer for boundary use.
## Implementation Notes
Schemas are declared at module scope and reused on every parse so Zod
does not rebuild the type graph per call. The tier enum is fixed to the
three values from design doc §2.2 (`trusted` / `isolated` / `sandboxed`)
— extending it requires an OpenSpec change. Capability handles only
carry the *method names* exposed by each capability proxy; the actual
`MessagePort` transfer happens out-of-band as part of the underlying
`worker_threads` channel setup.

View File

@@ -0,0 +1,92 @@
# Worker host conformance harness
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`runWorkerHostConformance` spins up an in-process worker-shaped peer
using paired `MessageChannel` ports. It exists so this package's own
test suite — and the lifecycle-driver tests inside `api-host` — can
exercise the full bootstrap → lifecycle → capability-proxy chain
without spawning a real `node:worker_threads` `Worker`. Two paired
ports are wired up (one for control, one for capability), the
bootstrap is run on the worker-side ports, a stub capability server
is mounted on the main-side of the capability port, and a main-side
RPC client is wired to the control port — giving callers everything
they need to drive lifecycle commands and observe capability call
dispatch deterministically.
## Features
- Optional overrides for `manifest`, `capabilityHandles`, plugin
`hooks` and the main-side `capabilityHandlers`.
- Reasonable defaults (`{ name: "test-plugin", version: "0.1.0",
tier: "isolated", capabilities: [] }`) so trivial tests need zero
setup.
- Returns the live `WorkerHost`, the main-side `WorkerRpcClient`,
the stub capability server, both port pairs and a single
`dispose()` that tears everything down in reverse order.
- No real `Worker` thread spawned — pure structured-clone routing
via `MessageChannel`.
## Usage: Basic
```ts
import {
runWorkerHostConformance,
} from "@suaveplan/api-worker-host/testing";
const h = runWorkerHostConformance({
hooks: { install: () => {} },
});
const ack = await h.mainClient.call("lifecycle.dispatch", [
{ command: "install" },
]);
h.dispose();
```
## Usage: Advanced
Provide capability handles + main-side handlers to round-trip a
capability call:
```ts
import {
runWorkerHostConformance,
} from "@suaveplan/api-worker-host/testing";
const h = runWorkerHostConformance({
capabilityHandles: { db: { methods: ["query"] } },
capabilityHandlers: {
"db.query": (params) => ({ rows: [params[0]] }),
},
});
const query = h.host.capabilities.db?.query;
const result = await query?.("SELECT 1");
h.dispose();
```
## API Reference
- `runWorkerHostConformance(opts?): WorkerHostConformanceHarness`
- `WorkerHostConformanceOptions`:
- `manifest?: ManifestEnvelope`
- `capabilityHandles?: CapabilityHandles`
- `hooks?: PluginLifecycleHooks`
- `capabilityHandlers?: WorkerRpcHandlers`
- `WorkerHostConformanceHarness`:
- `host: WorkerHost`
- `mainClient: WorkerRpcClient`
- `capabilityServer: WorkerRpcServer`
- `controlPorts / capabilityPorts: InProcessPortPair`
- `dispose(): void`
## Implementation Notes
`dispose()` closes the main-side client first so it stops sending,
then the worker host (which closes its own server + capability
client), then the main-side capability server, then both port pairs.
Closing in reverse-construction order avoids `port already closed`
log noise in vitest while still flushing the inflight RPC budget.