Files
2026-05-27 23:29:53 +01:00

65 lines
5.1 KiB
Markdown

# `@suaveplan/api-worker-rpc` — MessagePort protocol for capability marshalling
**Package**: `@suaveplan/api-worker-rpc`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 3 (Worker isolation infrastructure)
## Why
Every capability adapter ships two callable surfaces: a direct in-process one for `trusted` plugins and a `MessagePort`-marshalled one for `isolated` / `sandboxed` plugins (design doc §2.2, §5.2a). Hand-rolling that marshalling once per capability is how plugin frameworks accrete subtly-different RPC layers that diverge on cancellation, streaming, and error reconstruction. `api-worker-rpc` is the single, shared protocol every worker-proxy package builds on. Getting it right once means every capability — `db`, `events`, `secrets`, `jobs`, `logger`, future ones — gets cancellation, backpressure, streaming, and error reconstruction for free.
Per design doc §5.2a: every conformance suite runs against both surfaces. The proxy surface MUST behave identically to the direct surface from the plugin's perspective.
## What changes
Net-new package. Universal-tier (uses `MessagePort` from `worker_threads` on Node/Bun; the same `MessagePort` shape is also available in browsers, so the protocol is portable even though Stage-3 callers are all server-side).
**ADDED exports:**
- `./protocol` — wire types: `RpcRequest`, `RpcResponse`, `RpcError`, `RpcStreamChunk`, `RpcAbort`. All Zod-validated at the receive boundary.
- `./correlation` — monotonic message-id generator (`bigint`); request/response matching table; orphan-response detection.
- `./abort` — `AbortSignal` forwarding: caller-side `AbortController.signal` triggers an `RpcAbort` message; worker-side handler receives an `AbortSignal` that fires accordingly.
- `./streaming` — `ReadableStream` marshalling via `Transferable` (zero-copy chunk transfer); back-pressure carried in `RpcStreamChunk.ack`.
- `./error-reconstruction` — `SuaveplanError` rebuilt on the receiving side with the same `code`, `cause` chain (depth-bounded), and serialisable `details`. Non-`SuaveplanError` throws are wrapped in `RPC_REMOTE_THROW`.
- `./backpressure` — bounded outstanding-message budget per port; exceeding triggers `RPC_BACKPRESSURE` at the caller BEFORE exhausting memory.
- `./cloneable-guard` — pre-send check for structured-clone safety; non-cloneable values (functions, class instances without `serialize` hooks) throw `RPC_NOT_CLONEABLE` at the call site, not silently corrupting the wire.
- `./caller` — `createRpcCaller<T>(port)` returns a typed proxy of `T` whose method signatures are `WorkerProxyOf<T>` (return types wrapped in `Promise`).
- `./callee` — `serveRpc<T>(port, impl)` mounts an implementation on the receiving side; handles dispatch, abort wiring, and stream lifecycle.
- `./error-codes` — `RPC_*` codes via `defineCodes` (`RPC_BACKPRESSURE`, `RPC_NOT_CLONEABLE`, `RPC_REMOTE_THROW`, `RPC_TIMEOUT`, `RPC_PORT_CLOSED`, `RPC_ORPHAN_RESPONSE`, `RPC_PROTOCOL_VERSION_MISMATCH`).
- `./testing` — paired `MessageChannel` factory for in-process conformance suites.
## Out of scope
- **Capability-specific marshalling logic** — query-cursor handling for DB, log-record batching for logger, etc. Each worker-proxy package layers its own semantics on top of this protocol.
- **Process-level isolation** — `api-worker-rpc` is transport-only; spawning the worker is `api-worker-host`'s job.
- **Cross-host transport** — this is in-process `MessagePort` only. Cross-host fan-out is `api-ws-broker-*`'s domain.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Request/response correlation via monotonic ids | §5.2a |
| `AbortSignal` forwarding | §5.2a |
| Streaming via `Transferable` `ReadableStream` | §5.2a |
| Error reconstruction (`SuaveplanError` rebuilt) | §5.2a |
| Backpressure → `RPC_BACKPRESSURE` | §5.2a |
| Structured-clone-safe boundary → `RPC_NOT_CLONEABLE` | §5.2a |
## Verification approach
- `src/correlation/correlation.test.ts` — monotonic ids; request/response match; orphan detection.
- `src/abort/abort.test.ts` — caller-side abort propagates to worker `AbortSignal`; in-flight call rejects with `AbortError`.
- `src/streaming/streaming.test.ts` — large stream marshals in zero-copy chunks; backpressure ack throttles producer.
- `src/error-reconstruction/error-reconstruction.test.ts` — `SuaveplanError` round-trips with code + cause depth ≤ N; non-Suaveplan throw → `RPC_REMOTE_THROW`.
- `src/backpressure/backpressure.test.ts` — outstanding budget enforced; exceeding → `RPC_BACKPRESSURE`.
- `src/cloneable-guard/cloneable-guard.test.ts` — function argument → `RPC_NOT_CLONEABLE` at call site.
- `src/caller/caller.spec-conformance.test.ts` — `expectTypeOf` proves caller methods return `Promise<R>`.
- `tests/conformance/round-trip.test.ts` — pair `createRpcCaller` + `serveRpc` over `MessageChannel`; 50+ scenarios; every code in `error-codes.ts` reachable from at least one path.
## Migration impact
None — additive only. Stage-3 worker-proxy packages and `api-worker-host` will depend on this.