148 lines
9.4 KiB
Markdown
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
|
|
|