Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View 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"
```

View File

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

View File

@@ -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).

View File

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

View File

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

View File

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

View File

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