Initial Commit
This commit is contained in:
64
openspec/archive/2026-05-25-api-worker-rpc/proposal.md
Normal file
64
openspec/archive/2026-05-25-api-worker-rpc/proposal.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `@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.
|
||||
Reference in New Issue
Block a user