5.1 KiB
@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—AbortSignalforwarding: caller-sideAbortController.signaltriggers anRpcAbortmessage; worker-side handler receives anAbortSignalthat fires accordingly../streaming—ReadableStreammarshalling viaTransferable(zero-copy chunk transfer); back-pressure carried inRpcStreamChunk.ack../error-reconstruction—SuaveplanErrorrebuilt on the receiving side with the samecode,causechain (depth-bounded), and serialisabledetails. Non-SuaveplanErrorthrows are wrapped inRPC_REMOTE_THROW../backpressure— bounded outstanding-message budget per port; exceeding triggersRPC_BACKPRESSUREat the caller BEFORE exhausting memory../cloneable-guard— pre-send check for structured-clone safety; non-cloneable values (functions, class instances withoutserializehooks) throwRPC_NOT_CLONEABLEat the call site, not silently corrupting the wire../caller—createRpcCaller<T>(port)returns a typed proxy ofTwhose method signatures areWorkerProxyOf<T>(return types wrapped inPromise)../callee—serveRpc<T>(port, impl)mounts an implementation on the receiving side; handles dispatch, abort wiring, and stream lifecycle../error-codes—RPC_*codes viadefineCodes(RPC_BACKPRESSURE,RPC_NOT_CLONEABLE,RPC_REMOTE_THROW,RPC_TIMEOUT,RPC_PORT_CLOSED,RPC_ORPHAN_RESPONSE,RPC_PROTOCOL_VERSION_MISMATCH)../testing— pairedMessageChannelfactory 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-rpcis transport-only; spawning the worker isapi-worker-host's job. - Cross-host transport — this is in-process
MessagePortonly. Cross-host fan-out isapi-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 workerAbortSignal; in-flight call rejects withAbortError.src/streaming/streaming.test.ts— large stream marshals in zero-copy chunks; backpressure ack throttles producer.src/error-reconstruction/error-reconstruction.test.ts—SuaveplanErrorround-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_CLONEABLEat call site.src/caller/caller.spec-conformance.test.ts—expectTypeOfproves caller methods returnPromise<R>.tests/conformance/round-trip.test.ts— paircreateRpcCaller+serveRpcoverMessageChannel; 50+ scenarios; every code inerror-codes.tsreachable from at least one path.
Migration impact
None — additive only. Stage-3 worker-proxy packages and api-worker-host will depend on this.