Files
suaveplan/openspec/archive/2026-05-25-api-worker-rpc/proposal.md
2026-05-27 23:29:53 +01:00

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 — 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.