first commit
This commit is contained in:
78
docs/api-stack/api/api-router-worker-bridge/README.md
Normal file
78
docs/api-stack/api/api-router-worker-bridge/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# @suaveplan/api-router-worker-bridge — HTTP Request/Response marshalling across the worker boundary. Pairs a main-side and worker-side bridge over @suaveplan/api-worker-rpc, transporting fetch-compatible Request and Response objects (including ReadableStream bodies via streaming frames) without exposing the raw Request to worker code.
|
||||
|
||||
Stage-3 HTTP bridge that marshals Fetch `Request` and `Response` objects
|
||||
across the worker boundary. Pairs a main-side `dispatch` with a worker-
|
||||
side handler over a single `MessagePort`, transporting bodies either
|
||||
inline (small payloads) or via the `@suaveplan/api-worker-rpc` streaming
|
||||
frames (large payloads, with back-pressure).
|
||||
|
||||
The raw `Request` instance is **never** placed on the wire. Only the
|
||||
method, URL, allow-listed headers, and body descriptor cross the port;
|
||||
worker code reconstructs a fresh `Request` from those fields and can
|
||||
neither read the original socket nor inspect headers on the deny-list
|
||||
(`Authorization`, `Cookie`, `Set-Cookie`, `X-CSRF-Token`,
|
||||
`Proxy-Authorization` by default).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [bridge](./src/bridge/bridge.md) — This module ties `marshal-request`, `marshal-response`, and `streaming` together into the two halves of a working Fetch…
|
||||
- [bridge](./src/bridge/bridge.md) — This module ties `marshal-request`, `marshal-response`, and `streaming` together into the two halves of a working Fetch…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code registry for `@suaveplan/api-router-worker-bridge`. Every failure path that aris…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code registry for `@suaveplan/api-router-worker-bridge`. Every failure path that aris…
|
||||
- [marshal-request](./src/marshal-request/marshal-request.md) — This module converts a Fetch `Request` into a plain `SerializedRequest` object that is safe to ship across a `MessagePo…
|
||||
- [marshal-request](./src/marshal-request/marshal-request.md) — This module converts a Fetch `Request` into a plain `SerializedRequest` object that is safe to ship across a `MessagePo…
|
||||
- [marshal-response](./src/marshal-response/marshal-response.md) — This module is the response-side counterpart to `marshal-request`. It converts a Fetch `Response` produced by a worker-…
|
||||
- [marshal-response](./src/marshal-response/marshal-response.md) — This module is the response-side counterpart to `marshal-request`. It converts a Fetch `Response` produced by a worker-…
|
||||
- [streaming](./src/streaming/streaming.md) — This module marshals binary `ReadableStream<Uint8Array>` bodies between the main and worker processes by piggy-backing…
|
||||
- [streaming](./src/streaming/streaming.md) — This module marshals binary `ReadableStream<Uint8Array>` bodies between the main and worker processes by piggy-backing…
|
||||
- [testing](./src/testing/testing.md) — This module hands consumers the in-process scaffolding needed to exercise the worker bridge under a real `MessageChanne…
|
||||
- [testing](./src/testing/testing.md) — This module hands consumers the in-process scaffolding needed to exercise the worker bridge under a real `MessageChanne…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Modules
|
||||
|
||||
- [`marshal-request`](src/marshal-request/marshal-request.md) —
|
||||
`Request` ↔ `SerializedRequest` round-trip.
|
||||
- [`marshal-response`](src/marshal-response/marshal-response.md) —
|
||||
`Response` ↔ `SerializedResponse` round-trip.
|
||||
- [`streaming`](src/streaming/streaming.md) — `ReadableStream<Uint8Array>`
|
||||
transport via `stream-chunk` / `stream-end` / `stream-ack` frames.
|
||||
- [`bridge`](src/bridge/bridge.md) — main + worker bridge factories.
|
||||
- [`error-codes`](src/error-codes/error-codes.md) — frozen
|
||||
`ROUTER_BRIDGE.*` registry.
|
||||
- [`testing`](src/testing/testing.md) — in-process bridge + conformance
|
||||
harness.
|
||||
|
||||
## No-raw-Request invariant
|
||||
|
||||
`marshalRequest` returns a plain, structured-clone-safe object. Inspection
|
||||
of the wire envelope yields exactly: `method`, `url`, `headers`,
|
||||
`body { kind, ... }`. There is no `Request` reference and no way for
|
||||
worker code to call `request.clone()` on the original — the deny-list
|
||||
strips credentials before serialisation and the `Headers` instance handed
|
||||
to worker code is freshly constructed from the allow-listed tuples only.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/api-router-worker-bridge
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```ts
|
||||
import {
|
||||
createMainBridge,
|
||||
createWorkerBridge,
|
||||
} from "@suaveplan/api-router-worker-bridge/bridge";
|
||||
|
||||
const channel = new MessageChannel();
|
||||
createWorkerBridge(channel.port2 as never, async (req) =>
|
||||
new Response(`hi ${new URL(req.url).pathname}`)
|
||||
);
|
||||
const { dispatch } = createMainBridge(channel.port1 as never);
|
||||
const res = await dispatch(new Request("https://api.local/world"));
|
||||
console.log(await res.text()); // "hi /world"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# `bridge`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module ties `marshal-request`, `marshal-response`, and `streaming`
|
||||
together into the two halves of a working Fetch-`Request`-to-`Response`
|
||||
bridge over a single `MessagePort`. `createMainBridge` hands back a
|
||||
`dispatch(Request) → Promise<Response>` function that worker-isolated
|
||||
plugins consume; `createWorkerBridge` runs inside the worker and routes
|
||||
incoming envelopes to a user-supplied handler.
|
||||
|
||||
## Features
|
||||
|
||||
- Single bridge dispatch RPC method (`http.dispatch`); the main-side
|
||||
listener correlates responses by `id` so multiple in-flight requests
|
||||
multiplex over one port.
|
||||
- Optional request- and response-body streaming when `inlineBodyLimit`
|
||||
is set to `0`, threading through the `streaming` module.
|
||||
- Worker-side error rethrows are forwarded via `errorToEnvelope`, so the
|
||||
main side observes the original `SuaveplanError.code` chain.
|
||||
- `close()` on either side drops pending calls with a deterministic
|
||||
`ROUTER_BRIDGE.STREAM_INTERRUPTED` error rather than hanging.
|
||||
- Strict envelope validation on both ends — a malformed payload fails
|
||||
with `ROUTER_BRIDGE.MARSHAL_FAILED` or `RESPONSE_INVALID`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createMainBridge,
|
||||
createWorkerBridge,
|
||||
} from "@suaveplan/api-router-worker-bridge/bridge";
|
||||
|
||||
const { dispatch } = createMainBridge(mainPort);
|
||||
createWorkerBridge(workerPort, async (req) =>
|
||||
new Response(`hello ${new URL(req.url).pathname}`)
|
||||
);
|
||||
|
||||
const res = await dispatch(new Request("https://api.local/world"));
|
||||
await res.text(); // "hello /world"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire a streaming-only bridge for large uploads/downloads:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createMainBridge,
|
||||
createWorkerBridge,
|
||||
} from "@suaveplan/api-router-worker-bridge/bridge";
|
||||
|
||||
const { dispatch } = createMainBridge(mainPort, {
|
||||
inlineBodyLimit: 0,
|
||||
streamWindow: 32,
|
||||
});
|
||||
createWorkerBridge(
|
||||
workerPort,
|
||||
async (req) => new Response(req.body, { headers: req.headers }),
|
||||
{ inlineBodyLimit: 0, streamWindow: 32 }
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createMainBridge(port, options?) → { dispatch(Request): Promise<Response>, close(): void }`.
|
||||
- `createWorkerBridge(port, handler, options?) → { close(): void }`.
|
||||
- `BRIDGE_DISPATCH_METHOD = "http.dispatch"`.
|
||||
- `BridgeOptions { inlineBodyLimit?, maxBodyBytes?, headerDenyList?, streamWindow? }`.
|
||||
- `WorkerHandler = (Request) => Promise<Response> | Response`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The bridge intentionally re-uses `api-worker-rpc`'s request/response and
|
||||
stream frame shapes (and `PROTOCOL_VERSION`) so its traffic shares the
|
||||
same wire dialect — a downstream channel can multiplex both ordinary RPC
|
||||
and HTTP traffic without parsing ambiguity. When the request body is
|
||||
streamed, the same numeric id is used for both the RPC correlation and
|
||||
the stream id; that is safe because the streaming-frame `kind` is
|
||||
disjoint from the response frame and never collides.
|
||||
@@ -0,0 +1,64 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the stable error-code registry for `@suaveplan/api-router-worker-bridge`. Every failure path that arises while marshalling an HTTP `Request` or `Response` across the worker boundary — or while transporting body streams via `@suaveplan/api-worker-rpc`'s streaming frames — raises a typed `RouterBridgeError` carrying one of four namespaced codes. Consumers branch on the literal code value so production behaviour stays stable across patch releases.
|
||||
|
||||
## Features
|
||||
|
||||
- Single source of truth for `ROUTER_BRIDGE.*` codes (frozen tuple, never reordered).
|
||||
- `RouterBridgeError` extends `SuaveplanError` so it round-trips through `@suaveplan/api-worker-rpc`'s error reconstruction with its code preserved.
|
||||
- Codes are exhaustively typed via `RouterBridgeCode` so a `switch` on `err.code` is checked at compile time.
|
||||
- Stable string values are part of the public surface — adding a code is a minor bump; removing or renaming is a breaking change.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
ROUTER_BRIDGE_CODES,
|
||||
RouterBridgeError,
|
||||
} from "@suaveplan/api-router-worker-bridge/error-codes";
|
||||
|
||||
throw new RouterBridgeError(
|
||||
ROUTER_BRIDGE_CODES.MARSHAL_FAILED,
|
||||
"request method missing"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
ROUTER_BRIDGE_CODES,
|
||||
RouterBridgeError,
|
||||
} from "@suaveplan/api-router-worker-bridge/error-codes";
|
||||
|
||||
function handle(err: unknown): string {
|
||||
if (err instanceof RouterBridgeError) {
|
||||
switch (err.code) {
|
||||
case ROUTER_BRIDGE_CODES.BODY_TOO_LARGE:
|
||||
return "413";
|
||||
case ROUTER_BRIDGE_CODES.STREAM_INTERRUPTED:
|
||||
return "499";
|
||||
case ROUTER_BRIDGE_CODES.RESPONSE_INVALID:
|
||||
case ROUTER_BRIDGE_CODES.MARSHAL_FAILED:
|
||||
return "500";
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ROUTER_BRIDGE_CODES` — frozen record of `"ROUTER_BRIDGE.<NAME>"` strings.
|
||||
- `RouterBridgeCode` — union of every member of `ROUTER_BRIDGE_CODES`.
|
||||
- `RouterBridgeError` — `SuaveplanError<RouterBridgeCode>` subclass with `name === "RouterBridgeError"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry uses `defineCodes("ROUTER_BRIDGE", [...] as const)` so TypeScript narrows the value union; the `as const` tuple is non-negotiable. `RouterBridgeError` overrides `name` so structured logging and cross-port reconstruction can identify the class without `instanceof` (which is unreliable across realm boundaries).
|
||||
@@ -0,0 +1,82 @@
|
||||
# `marshal-request`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module converts a Fetch `Request` into a plain `SerializedRequest`
|
||||
object that is safe to ship across a `MessagePort`. The raw `Request`
|
||||
instance NEVER appears on the wire: only the method, URL, an allow-listed
|
||||
header tuple list, and a body descriptor are serialised. The body is
|
||||
either absent, inlined as `Uint8Array` (small requests), or replaced by a
|
||||
`streamId` reference that the streaming module wires up over
|
||||
`@suaveplan/api-worker-rpc`.
|
||||
|
||||
## Features
|
||||
|
||||
- Strips credentials-bearing headers (`Authorization`, `Cookie`, etc.) by
|
||||
default; the deny-list is overrideable but cannot be silently bypassed.
|
||||
- Inlines bodies up to `DEFAULT_INLINE_BODY_LIMIT` (64 KiB); larger bodies
|
||||
REQUIRE a caller-supplied `streamId` and are paired with the streaming
|
||||
module's frames.
|
||||
- Enforces a hard `DEFAULT_MAX_BODY_BYTES` cap (16 MiB) by raising
|
||||
`RouterBridgeError(ROUTER_BRIDGE.BODY_TOO_LARGE)`.
|
||||
- Provides `rehydrateRequest` on the worker side that reconstructs a real
|
||||
Fetch `Request` (including `duplex: "half"` for streamed bodies).
|
||||
- Round-trip Zod schema (`serializedRequestSchema`) so envelope validation
|
||||
on either end fails with a precise issue tree.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { marshalRequest, rehydrateRequest } from
|
||||
"@suaveplan/api-router-worker-bridge/marshal-request";
|
||||
|
||||
const wire = await marshalRequest(
|
||||
new Request("https://api.example.com/v1/ping", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", "authorization": "Bearer x" },
|
||||
body: JSON.stringify({ ok: true }),
|
||||
})
|
||||
);
|
||||
// wire.headers has no "authorization" entry
|
||||
const reborn = rehydrateRequest(wire);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Streaming a large body via a separate stream id:
|
||||
|
||||
```ts
|
||||
import {
|
||||
marshalRequest,
|
||||
rehydrateRequest,
|
||||
} from "@suaveplan/api-router-worker-bridge/marshal-request";
|
||||
|
||||
const streamId = 42;
|
||||
const wire = await marshalRequest(largeUploadRequest, {
|
||||
streamId,
|
||||
inlineBodyLimit: 0,
|
||||
});
|
||||
// caller pipes `largeUploadRequest.body` over the streaming channel
|
||||
// keyed on `streamId`; worker side calls:
|
||||
const reborn = rehydrateRequest(wire, workerStreamFor(streamId));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `marshalRequest(request, options?) → Promise<SerializedRequest>`.
|
||||
- `rehydrateRequest(serialized, streamBody?) → Request`.
|
||||
- `serializedRequestSchema` — Zod schema validating the envelope shape.
|
||||
- `DEFAULT_INLINE_BODY_LIMIT`, `DEFAULT_MAX_BODY_BYTES`, `DEFAULT_HEADER_DENY_LIST`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The serialised body is a tagged union (`"none" | "inline" | "stream"`); a
|
||||
single discriminant avoids the ambiguity of treating `byteLength === 0` as
|
||||
"no body". `request.headers.forEach` is used instead of spreading entries
|
||||
because cross-runtime `Headers` iteration is more reliable that way.
|
||||
`duplex: "half"` is only set when reconstructing a request with a body —
|
||||
Node refuses the flag otherwise.
|
||||
@@ -0,0 +1,77 @@
|
||||
# `marshal-response`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the response-side counterpart to `marshal-request`. It
|
||||
converts a Fetch `Response` produced by a worker-side handler into a
|
||||
plain `SerializedResponse` envelope safe to ship across a `MessagePort`,
|
||||
and rehydrates that envelope back into a real `Response` on the main
|
||||
process so the host pipeline can apply Steps 9-11 (response validation,
|
||||
ETag, Server-Timing aggregation).
|
||||
|
||||
## Features
|
||||
|
||||
- Validates status code is in the 100-599 range (`ROUTER_BRIDGE.RESPONSE_INVALID`).
|
||||
- Preserves status text and full header list (response headers are not on
|
||||
any deny-list — the bridge only filters request-side credentials).
|
||||
- Inlines bodies up to `DEFAULT_RESPONSE_INLINE_LIMIT` (64 KiB) and
|
||||
streams everything else through the streaming module.
|
||||
- Hard cap at `DEFAULT_RESPONSE_MAX_BYTES` (16 MiB) for inline bodies;
|
||||
oversize payloads either stream or fail with
|
||||
`ROUTER_BRIDGE.BODY_TOO_LARGE`.
|
||||
- Tagged-union body shape mirrors the request side for symmetry.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { marshalResponse, rehydrateResponse } from
|
||||
"@suaveplan/api-router-worker-bridge/marshal-response";
|
||||
|
||||
const wire = await marshalResponse(
|
||||
new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
);
|
||||
const reborn = rehydrateResponse(wire);
|
||||
await reborn.json(); // { ok: true }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Stream a large attachment response via a stream id:
|
||||
|
||||
```ts
|
||||
import {
|
||||
marshalResponse,
|
||||
rehydrateResponse,
|
||||
} from "@suaveplan/api-router-worker-bridge/marshal-response";
|
||||
|
||||
const streamId = 99;
|
||||
const wire = await marshalResponse(largeDownload, {
|
||||
streamId,
|
||||
inlineBodyLimit: 0,
|
||||
});
|
||||
// caller pipes `largeDownload.body` over the streaming channel; main side:
|
||||
const reborn = rehydrateResponse(wire, mainStreamFor(streamId));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `marshalResponse(response, options?) → Promise<SerializedResponse>`.
|
||||
- `rehydrateResponse(serialized, streamBody?) → Response`.
|
||||
- `serializedResponseSchema` — Zod validator.
|
||||
- `DEFAULT_RESPONSE_INLINE_LIMIT`, `DEFAULT_RESPONSE_MAX_BYTES`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`statusText` is preserved because some clients (and HTTP/1.1 caches)
|
||||
still depend on the reason phrase. The body tagged union is identical in
|
||||
shape to `SerializedRequestBody`, but the two types are intentionally
|
||||
distinct so future divergence (e.g. server-sent-event hints) does not
|
||||
silently propagate. Status validation runs first so a `100/600` test
|
||||
case fails cleanly even when the body would also fail.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `streaming`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module marshals binary `ReadableStream<Uint8Array>` bodies between
|
||||
the main and worker processes by piggy-backing on `@suaveplan/api-worker-
|
||||
rpc`'s `stream-chunk` / `stream-end` / `stream-ack` frames. It implements
|
||||
the producer (`sendStream`) and consumer (`receiveStream`) halves of a
|
||||
single body transfer keyed on a numeric `streamId` (the same id baked
|
||||
into the body descriptor on the request or response envelope).
|
||||
|
||||
## Features
|
||||
|
||||
- Window-based back-pressure: the producer stalls after `DEFAULT_STREAM_
|
||||
WINDOW` (16) outbound chunks until the consumer sends a matching
|
||||
`stream-ack`, preventing unbounded buffering across the port.
|
||||
- Reuses the wire frame shape of `api-worker-rpc` so a single port can
|
||||
multiplex RPC and bridge streams interchangeably.
|
||||
- Producer errors are forwarded via `errorToEnvelope`; consumer rehydrates
|
||||
via `envelopeToError`, preserving the `SuaveplanError` code chain.
|
||||
- Defensive enqueue: non-binary chunks fail with `ROUTER_BRIDGE.STREAM_
|
||||
INTERRUPTED` so misuse is loud rather than silently corrupting the body.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
sendStream,
|
||||
receiveStream,
|
||||
} from "@suaveplan/api-router-worker-bridge/streaming";
|
||||
|
||||
const stream = receiveStream(port, 1);
|
||||
// consume `stream` like any ReadableStream
|
||||
await sendStream(port, 1, sourceStream);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Tune the back-pressure window for a high-throughput download path:
|
||||
|
||||
```ts
|
||||
import { sendStream } from "@suaveplan/api-router-worker-bridge/streaming";
|
||||
|
||||
await sendStream(port, streamId, hugeAttachment.body!, { window: 64 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `sendStream(port, streamId, stream, options?) → Promise<void>`.
|
||||
- `receiveStream(port, streamId, options?) → ReadableStream<Uint8Array>`.
|
||||
- `DEFAULT_STREAM_WINDOW = 16`.
|
||||
- `StreamOptions { window?: number }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The producer's ack listener is registered for the entire pump lifetime and
|
||||
torn down in `finally` so a crash in the source stream cannot leak a
|
||||
listener. `receiveStream` issues a `stream-ack` whenever its locally-
|
||||
buffered count reaches the window, not after every chunk — this matches
|
||||
the producer's pause threshold so throughput is steady rather than
|
||||
sawtoothed. `ArrayBuffer` payloads are coerced to `Uint8Array` to absorb
|
||||
runtime variation in how `MessagePort` surfaces transfers.
|
||||
@@ -0,0 +1,69 @@
|
||||
# `testing`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module hands consumers the in-process scaffolding needed to exercise
|
||||
the worker bridge under a real `MessageChannel` without spinning up an
|
||||
actual worker thread, plus a conformance harness that every alternate
|
||||
transport (in-process today, worker-thread tomorrow) must satisfy.
|
||||
|
||||
## Features
|
||||
|
||||
- `createInProcessBridge(handler, options?)` returns a ready-to-use pair:
|
||||
the main-side `dispatch`, the underlying ports, and a shared `close`
|
||||
that tears down both halves and the channel.
|
||||
- `runBridgeConformance(runner, factory)` exercises round-trip GET, JSON
|
||||
POST, and the header deny-list invariant. Test-runner-agnostic via the
|
||||
injected `ConformanceRunner` shape.
|
||||
- Built on `@suaveplan/api-worker-rpc/testing`'s
|
||||
`createInProcessPortPair`, so the conformance suite stays aligned with
|
||||
the underlying RPC contract.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createInProcessBridge } from
|
||||
"@suaveplan/api-router-worker-bridge/testing";
|
||||
|
||||
const bridge = createInProcessBridge(async () => new Response("pong"));
|
||||
const res = await bridge.dispatch(new Request("https://api.local/ping"));
|
||||
console.log(await res.text()); // "pong"
|
||||
bridge.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Plug into Vitest's conformance gates:
|
||||
|
||||
```ts
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
createInProcessBridge,
|
||||
runBridgeConformance,
|
||||
} from "@suaveplan/api-router-worker-bridge/testing";
|
||||
|
||||
runBridgeConformance(
|
||||
{ describe, it, expect: expect as unknown as never },
|
||||
(handler) => createInProcessBridge(handler)
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createInProcessBridge(handler, options?) → InProcessBridge`.
|
||||
- `InProcessBridge { ports, dispatch, close }`.
|
||||
- `runBridgeConformance(runner, factory) → void`.
|
||||
- `ConformanceRunner { describe(name, body), it(name, opts, body), expect }`
|
||||
— matches vitest 4 / bun:test signatures.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The harness intentionally only asserts the invariants that ALL bridge
|
||||
implementations share. Transport-specific behaviour (e.g. worker-thread
|
||||
spin-up costs, transferList semantics, restart-on-crash) belongs in the
|
||||
transport package's own test file. Each scenario tears down via
|
||||
`bridge.close()` in `finally` to avoid leaking ports across tests.
|
||||
Reference in New Issue
Block a user