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

148 lines
9.4 KiB
Markdown

# @suaveplan/api-worker-rpc - MessagePort-based RPC protocol for worker-isolated API plugins. Provides Zod-validated wire frames, monotonic correlation ids, AbortSignal forwarding, streaming with backpressure, structured-clone safety guards, SuaveplanError reconstruction across the boundary, and paired client/server bindings. Universal; Stage-3 worker-proxy packages depend on this.
MessagePort-based RPC protocol for worker-isolated API plugins. Provides
the shared protocol — Zod-validated wire frames, monotonic correlation
ids, AbortSignal forwarding, streaming with backpressure, structured-
clone safety guards, SuaveplanError reconstruction across the boundary,
and paired client/server bindings — that every Stage-3 worker-proxy
package builds on.
The package is universal: it depends only on a structural
`PortLike` shape that matches both Node/Bun's `worker_threads`
`MessagePort` and the Web platform's `MessagePort`. No platform
globals are imported at module load.
<!-- begin:auto-docs -->
## Documentation tree
- [abort-forwarding](./src/abort-forwarding/abort-forwarding.md) — Wires caller-side `AbortSignal` instances across the `MessagePort` boundary so an in-flight RPC can be cancelled cooper…
- [abort-forwarding](./src/abort-forwarding/abort-forwarding.md) — Wires caller-side `AbortSignal` instances across the `MessagePort` boundary so an in-flight RPC can be cancelled cooper…
- [backpressure](./src/backpressure/backpressure.md) — Bounds memory growth on a busy RPC channel by exposing two primitives: the logical `CallBudget` for outstanding in-flig…
- [backpressure](./src/backpressure/backpressure.md) — Bounds memory growth on a busy RPC channel by exposing two primitives: the logical `CallBudget` for outstanding in-flig…
- [client](./src/client/client.md) — Caller-side binding for the worker-RPC protocol. `createRawWorkerRpcClient` returns a low-level object exposing `.call(…
- [client](./src/client/client.md) — Caller-side binding for the worker-RPC protocol. `createRawWorkerRpcClient` returns a low-level object exposing `.call(…
- [cloneable-guard](./src/client/cloneable-guard.md) — Walks a value (typically an RPC argument list or return value) and throws `WORKER_RPC.NOT_CLONEABLE` at the call site i…
- [correlation](./src/correlation/correlation.md) — Provides the monotonic correlation-id generator and the in-memory pending-call registry that backs every outstanding RP…
- [correlation](./src/correlation/correlation.md) — Provides the monotonic correlation-id generator and the in-memory pending-call registry that backs every outstanding RP…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable `WORKER_RPC.*` code registry and the `WorkerRpcError` class. Every public failure path in `@suavepla…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable `WORKER_RPC.*` code registry and the `WorkerRpcError` class. Every public failure path in `@suavepla…
- [error-reconstruction](./src/error-reconstruction/error-reconstruction.md) — Translates thrown values into wire-safe `ErrorEnvelope`s and back into typed `Error` instances on the receiving side. `…
- [error-reconstruction](./src/error-reconstruction/error-reconstruction.md) — Translates thrown values into wire-safe `ErrorEnvelope`s and back into typed `Error` instances on the receiving side. `…
- [protocol](./src/protocol/protocol.md) — Defines the wire-format of every message that crosses a `MessagePort` between an `api-host` and an isolated/sandboxed p…
- [protocol](./src/protocol/protocol.md) — Defines the wire-format of every message that crosses a `MessagePort` between an `api-host` and an isolated/sandboxed p…
- [server](./src/server/server.md) — Worker-side binding for the RPC protocol. `serveWorkerRpc(port, handlers)` mounts a handlers object on a `MessagePort`,…
- [server](./src/server/server.md) — Worker-side binding for the RPC protocol. `serveWorkerRpc(port, handlers)` mounts a handlers object on a `MessagePort`,…
- [streaming](./src/streaming/streaming.md) — Caller-side primitives for consuming streaming RPC methods. The server publishes a `stream-chunk` frame per yielded val…
- [streaming](./src/streaming/streaming.md) — Caller-side primitives for consuming streaming RPC methods. The server publishes a `stream-chunk` frame per yielded val…
- [testing](./src/testing/testing.md) — Reusable conformance harness and an in-process paired-port factory used by every Stage-3 worker-proxy package's test su…
- [conformance](./src/testing/conformance.md) — Builds a paired client/server harness over an in-process `MessageChannel` for conformance tests. The harness is intende…
- [testing](./src/testing/testing.md) — Reusable conformance harness and an in-process paired-port factory used by every Stage-3 worker-proxy package's test su…
<!-- end:auto-docs -->
## Why
Per the api-plugin system design (§2.2 isolation tiers, §5.2a
worker boundary), every capability adapter ships in two surfaces: a
direct in-process binding for `trusted` plugins and a `MessagePort`-
marshalled binding for `isolated` / `sandboxed` plugins. 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.
## Install
```bash
bun add @suaveplan/api-worker-rpc
```
## Quick start
```ts
import { createWorkerRpcClient } from "@suaveplan/api-worker-rpc";
import { serveWorkerRpc } from "@suaveplan/api-worker-rpc";
import { createInProcessPortPair } from "@suaveplan/api-worker-rpc/testing";
interface Calc { add(a: number, b: number): number; }
const { clientPort, serverPort, close } = createInProcessPortPair();
const server = serveWorkerRpc(serverPort, {
add: ([a, b]) => (a as number) + (b as number),
});
const calc = createWorkerRpcClient<Calc>(clientPort);
console.log(await calc.add(2, 3)); // 5
calc.close();
server.close();
close();
```
## Modules
- [`protocol`](./src/protocol/protocol.md) — Zod-validated wire frames
(`request`, `response`, `cancel`, `progress`, `stream-open`,
`stream-chunk`, `stream-end`, `stream-ack`, `hello`, `goodbye`)
plus `parseFrame` and the `ErrorEnvelope` type.
- [`correlation`](./src/correlation/correlation.md) — monotonic id
generator + `PendingRegistry` (orphan detection, reuse detection,
timeout eviction).
- [`client`](./src/client/client.md) — `createWorkerRpcClient<T>` typed
proxy and `createRawWorkerRpcClient` low-level binding.
- [`client/cloneable-guard`](./src/client/cloneable-guard.md) —
pre-send structured-clone safety walk.
- [`server`](./src/server/server.md) — `serveWorkerRpc(port,
handlers)` mounts handler maps with automatic streaming and
cancel-via-`AbortController`.
- [`streaming`](./src/streaming/streaming.md) — caller-side
`streamRequest` returning an `AsyncIterable` + `StreamDispatcher`
for inbound-frame routing.
- [`abort-forwarding`](./src/abort-forwarding/abort-forwarding.md) —
`attachAbortSignal`, `createRemoteAbort`, and the platform-independent
`AbortError` class.
- [`backpressure`](./src/backpressure/backpressure.md) —
`CallBudget` (logical) and `BoundedQueue` (physical) with three
policies.
- [`error-reconstruction`](./src/error-reconstruction/error-reconstruction.md)
— `errorToEnvelope` / `envelopeToError` with depth-bounded cause
chains.
- [`error-codes`](./src/error-codes/error-codes.md) — the
`WORKER_RPC_CODES` registry and `WorkerRpcError` class.
- [`testing`](./src/testing/testing.md) — `createInProcessPortPair`
+ `runWorkerRpcConformance` harness.
## Error codes
Every code in `WORKER_RPC_CODES` is reachable from at least one
production path:
| Code | Raised from |
| --------------------------------- | ------------------------------------------- |
| `WORKER_RPC.PORT_CLOSED` | `client.close()` reject; post-close send |
| `WORKER_RPC.REQUEST_TIMEOUT` | per-call timeout eviction |
| `WORKER_RPC.RESPONSE_TIMEOUT` | (reserved for response-only timeout policy) |
| `WORKER_RPC.INVALID_FRAME` | malformed inbound frame |
| `WORKER_RPC.UNKNOWN_METHOD` | server dispatch of missing handler |
| `WORKER_RPC.CORRELATION_ID_REUSED`| double-register on same id |
| `WORKER_RPC.STREAM_BACKPRESSURE_EXCEEDED` | reserved for stream-explicit overflow |
| `WORKER_RPC.HANDLER_THREW` | server-side stream handler not iterable |
| `WORKER_RPC.CANCELLED` | registry cancel |
| `WORKER_RPC.VERSION_MISMATCH` | inbound `v !== PROTOCOL_VERSION` |
| `WORKER_RPC.ORPHAN_RESPONSE` | `settle()` on unknown id |
| `WORKER_RPC.NOT_CLONEABLE` | `assertCloneable` pre-send walk |
| `WORKER_RPC.REMOTE_THROW` | wrapping foreign throws on the wire |
| `WORKER_RPC.BACKPRESSURE` | `CallBudget` or fail-mode `BoundedQueue` |
## Verification
- 100% test coverage gated in CI (`bunx turbo test:coverage --filter=@suaveplan/api-worker-rpc`).
- Full lint+typecheck+test+build gate per package rules.
## License
MIT