first commit
This commit is contained in:
100
docs/api-stack/api/api-worker-host/src/bootstrap/bootstrap.md
Normal file
100
docs/api-stack/api/api-worker-host/src/bootstrap/bootstrap.md
Normal 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.
|
||||
@@ -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".
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
92
docs/api-stack/api/api-worker-host/src/testing/testing.md
Normal file
92
docs/api-stack/api/api-worker-host/src/testing/testing.md
Normal 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.
|
||||
Reference in New Issue
Block a user