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,86 @@
# Worker-Side Jobs Proxy Client
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createJobsProxyClient(port, opts)` is the worker-tier entry point for the
`@suaveplan/api-jobs-worker-proxy` package. It returns an object that
implements the `JobRegistrationHandle` capability slot from
`@suaveplan/api-types`, letting a worker-isolated API plugin register
cron and queued jobs even though the actual scheduler runs in the host
main process. Every `registerCron` and `registerQueued` call stores the
handler in a local `HandlerRegistry` and asynchronously marshals a
`registerJob` RPC to the main side via the supplied `MessagePort`. When
the main scheduler ticks a job, the same port carries a `runJob`
dispatch back to the worker; the local serve-side listener invokes the
stored handler under an optional timeout.
## Features
- Implements the exact `JobRegistrationHandle` surface (`registerCron`,
`registerQueued`) — no proxy-specific extensions on the public type.
- Bidirectional over a single port: outbound registration RPCs +
inbound `runJob` dispatch via paired raw RPC client + serve handler.
- Fire-and-forget registration semantics with an `onHandlerError`
observer for asynchronous rejections (job-not-declared, port closed).
- Per-handler timeout via `handlerTimeoutMs`; long-running closures
reject with `JOBS_PROXY.JOB_TIMEOUT`.
- Idempotent `close(reason?)` tears down both the RPC client and the
serve listener.
## Usage: Basic
```ts
import { MessageChannel } from "node:worker_threads";
import { createJobsProxyClient } from "@suaveplan/api-jobs-worker-proxy/worker-side";
const channel = new MessageChannel();
const client = createJobsProxyClient(channel.port1);
client.registerCron("rotate-keys", async () => {
await rotate();
});
```
## Usage: Advanced
```ts
import { createJobsProxyClient } from "@suaveplan/api-jobs-worker-proxy/worker-side";
const client = createJobsProxyClient(workerPort, {
handlerTimeoutMs: 30_000,
onHandlerError: (err, ctx) => {
logger.warn({ code: err.code, name: ctx.name, kind: ctx.kind });
},
});
client.registerQueued("send-email", async (payload) => {
await mailer.send(payload as MailPayload);
});
```
## API Reference
- `createJobsProxyClient(port, opts?) -> JobsProxyClient` — factory.
- `JobsProxyClient extends JobRegistrationHandle` — adds `close()` and a
`sizes` accessor reporting registration counts.
- `JobsProxyClientOptions`:
- `handlerTimeoutMs?: number` — per-dispatch timeout, `0` disables.
- `onHandlerError?: (err, ctx) => void` — observer hook.
## Implementation Notes
The client multiplexes a `createRawWorkerRpcClient` (for outbound
`registerJob` calls) and a `serveWorkerRpc` handlers object (for inbound
`runJob`) over the same `PortLike`. Protocol frames partition cleanly:
`response` frames target the RPC client's pending registry, and
`request` frames target the serve dispatcher; each listener silently
drops the other's traffic. The local `HandlerRegistry` is the single
source of truth for which handlers can be dispatched — the main side
relies on the worker holding the closure and we never serialize the
function across the wire. The fire-and-forget `registerJob` call uses
`.catch` to funnel rejections (including `JOB_NOT_DECLARED` from main)
through `onHandlerError`, preserving the original `JobRegistrationHandle`
synchronous-`void` return contract.

View File

@@ -0,0 +1,68 @@
# Error Codes — `JOBS_PROXY.*`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the stable error-code registry for
`@suaveplan/api-jobs-worker-proxy`. Every public failure path raises a
`JobsProxyError` carrying one of the namespaced `JOBS_PROXY.*` codes,
so consumers can branch on the literal string without depending on
class identity (which is fragile across worker-boundary
reconstruction). The registry is defined once via `defineCodes` to keep
typing tight and prevent accidental drift between docs and runtime.
## Features
- Five stable codes covering the worker ↔ main bridge failure surface.
- `JobsProxyError extends SuaveplanError<JobsProxyCode>` for typed
discrimination via `instanceof` AND `.code`.
- `as const` registry so TypeScript narrows literal codes at use sites.
## Usage: Basic
```ts
import { JobsProxyError, JOBS_PROXY_CODES } from "@suaveplan/api-jobs-worker-proxy/error-codes";
throw new JobsProxyError(
JOBS_PROXY_CODES.JOB_NOT_DECLARED,
"job rotate-keys is not in the manifest",
);
```
## Usage: Advanced
```ts
try {
await client.tickCron("rotate-keys");
} catch (err) {
if (err instanceof JobsProxyError && err.code === "JOBS_PROXY.JOB_TIMEOUT") {
metrics.increment("jobs.timeouts", { name: "rotate-keys" });
}
throw err;
}
```
## API Reference
- `JOBS_PROXY_CODES` — readonly registry of five string codes:
- `JOB_NOT_DECLARED` — name absent from manifest whitelist.
- `HANDLER_THREW` — worker-side handler threw or rejected.
- `PROXY_DISPOSED` — operation attempted after `close()`.
- `JOB_TIMEOUT` — dispatch exceeded `handlerTimeoutMs`.
- `DUPLICATE_REGISTRATION` — same name registered twice.
- `JobsProxyCode` — union of every literal value in the registry.
- `JobsProxyError(code, message, options?)` — typed error class.
## Implementation Notes
`defineCodes("JOBS_PROXY", [...])` (from `@suaveplan/error`) generates an
object whose values are the fully-qualified codes (e.g.
`"JOBS_PROXY.HANDLER_THREW"`). The `JobsProxyCode` type derives from
this object so adding a new code requires only editing the tuple — type
checking surfaces every site that switches on the code. The class sets
`name = "JobsProxyError"` so serialised error envelopes carrying this
class name can be discriminated downstream even when reconstruction
flattens to a generic `SuaveplanError` subclass.

View File

@@ -0,0 +1,68 @@
# Handler Bridge
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the shared wire schemas, the worker-side
`HandlerRegistry`, and the `runWithTimeout` helper used by both ends of
the proxy. The bridge is the only place that decides how a registered
cron/queued handler is exercised on the worker side; isolating it keeps
the client and server modules small and lets the same logic be unit
tested without spinning up a `MessageChannel`. Schemas use `zod` so any
bad payload that crosses the boundary fails fast at the parse step
rather than producing an obscure runtime error downstream.
## Features
- `registerJobRequestSchema` / `runJobRequestSchema` — Zod-validated
wire formats for registration and dispatch frames.
- `HandlerRegistry` — type-safe storage of cron + queued closures with
idempotent set / run / clear semantics and a `sizes` accessor.
- `runWithTimeout` — runs a closure with an optional millisecond budget
and wraps every failure into a typed `JobsProxyError` (`HANDLER_THREW`
or `JOB_TIMEOUT`).
## Usage: Basic
```ts
import { HandlerRegistry } from "@suaveplan/api-jobs-worker-proxy/handler-bridge";
const reg = new HandlerRegistry();
reg.setCron("daily-rotate", async () => rotate());
await reg.runCron("daily-rotate");
```
## Usage: Advanced
```ts
import { runWithTimeout } from "@suaveplan/api-jobs-worker-proxy/handler-bridge";
await runWithTimeout(
() => longRunningExport(),
"export-csv",
30_000,
); // throws JobsProxyError("JOBS_PROXY.JOB_TIMEOUT") after 30s
```
## API Reference
- `registerJobRequestSchema: ZodType<RegisterJobRequest>` — wire schema.
- `runJobRequestSchema: ZodType<RunJobRequest>` — wire schema.
- `HandlerRegistry` — `setCron`, `setQueued`, `hasCron`, `hasQueued`,
`runCron`, `runQueued`, `clear`, `sizes`.
- `runWithTimeout(fn, name, timeoutMs)` — runs `fn`, wraps failures.
## Implementation Notes
`runWithTimeout` uses `Promise.race` between the handler invocation and
a timeout-rejecting promise. The timeout branch always installs a timer
(non-nullable) when `timeoutMs > 0`; the finally-clear is unconditional
once that branch is entered. A `0` budget skips the race entirely and
falls back to a direct `await` so successful handlers do not pay the
extra microtask cost. `HandlerRegistry.setCron`/`setQueued` return a
boolean indicating whether the name already existed, allowing callers to
detect (and warn on) duplicate registrations without forbidding the
replacement.

View File

@@ -0,0 +1,86 @@
# Main-Side Jobs Proxy Server
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createJobsProxyServer(port, mainHandle, opts)` runs in the host main
process. It receives `registerJob` RPC frames from a worker-side
`createJobsProxyClient` over the supplied `MessagePort`, validates the
incoming job name against the plugin's manifest-declared jobs, and
forwards the registration to the host's real
`JobRegistrationHandle` (which in practice wraps
`@suaveplan/job-scheduler`). Each successful registration is bound to a
closure that, when ticked by the scheduler, dispatches a `runJob` RPC
back to the worker. This is The piece that lets a plugin's
worker-isolated code participate in the main-process cron/queue
lifecycle without sharing memory.
## Features
- Manifest-bound: `declaredJobs` is the authoritative whitelist. Names
not in the list reject with `JOBS_PROXY.JOB_NOT_DECLARED`.
- Kind enforcement: registering a name as `cron` when the manifest
declares it `queued` (or vice versa) is also rejected as
`JOB_NOT_DECLARED`.
- Optional per-dispatch RPC timeout (`runRpcTimeoutMs`) so a hung
worker handler does not stall scheduler ticks indefinitely.
- `onDispatchError` observer for telemetry on failed worker
invocations.
- Idempotent `close()` tears down the inbound listener AND the outbound
RPC client.
- `registeredCount` accessor for tests + diagnostics.
## Usage: Basic
```ts
import { MessageChannel } from "node:worker_threads";
import { createJobsProxyServer } from "@suaveplan/api-jobs-worker-proxy/main-side";
const server = createJobsProxyServer(channel.port2, hostScheduler, {
declaredJobs: [{ name: "rotate-keys", kind: "cron" }],
});
```
## Usage: Advanced
```ts
import { createJobsProxyServer } from "@suaveplan/api-jobs-worker-proxy/main-side";
const server = createJobsProxyServer(channel.port2, hostScheduler, {
declaredJobs: manifest.jobs,
runRpcTimeoutMs: 60_000,
onDispatchError: (err, ctx) => metrics.increment("jobs.dispatch_failed", { name: ctx.name }),
});
// Later, on teardown:
server.close("plugin-unload");
```
## API Reference
- `createJobsProxyServer(port, mainHandle, opts) -> JobsProxyServer`.
- `JobsProxyServer`:
- `close(reason?: string): void` — idempotent disposer.
- `registeredCount: number` — count of accepted registrations.
- `JobsProxyServerOptions`:
- `declaredJobs: ReadonlyArray<{name, kind}>` — manifest whitelist.
- `runRpcTimeoutMs?: number` — optional per-tick RPC timeout.
- `onDispatchError?: (err, ctx) => void` — telemetry hook.
## Implementation Notes
The server mounts a `serveWorkerRpc` handlers object exposing a single
`registerJob` method that parses incoming payloads with
`registerJobRequestSchema`. The manifest whitelist is normalised into a
`Map<name, kind>` at construction time for O(1) lookups. When a
registration is accepted, the closure stored with the host scheduler
captures the supplied `name`/`kind` and, on tick, invokes
`rpc.call("runJob", [{name, kind, payload}])` via a paired
`createRawWorkerRpcClient`. The dispatcher checks `closed` first so a
late tick after `close()` raises `JOBS_PROXY.PROXY_DISPOSED` instead of
posting on a dead port. The server intentionally has no opinion about
backpressure: the underlying RPC layer already supplies a bounded
outbound queue.

View File

@@ -0,0 +1,84 @@
# Testing Harness and Conformance Suite
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The testing module exposes a fully-wired in-process harness so consumers
can exercise the worker-proxy surface without launching real workers,
plus a conformance suite that codifies the invariants every
`JobRegistrationHandle` proxy must satisfy. Because
`@suaveplan/job-scheduler` does not yet ship a `runJobsConformance`
harness, the canonical Stage-4 gate referred to in the proposal is
provided locally here; downstream packages can replace it transparently
once the upstream harness lands without touching the public surface.
## Features
- `StubJobScheduler` — implements `JobRegistrationHandle` with imperative
`tickCron(name)` / `tickQueued(name, payload)` methods for tests.
- `createJobsProxyHarness(opts)` — bundles a `MessageChannel`, the stub
scheduler, a `JobsProxyServer`, and a `JobsProxyClient` into a single
closable handle with a `flush()` helper.
- `JOBS_CONFORMANCE_CASES` + `runJobsConformance(factory)` — three
scenarios covering cron round-trip, queued round-trip with payload,
and rejection of non-declared names.
- `emptyHandler` — exported no-op cron closure used by negative-path
scenarios.
## Usage: Basic
```ts
import { createJobsProxyHarness } from "@suaveplan/api-jobs-worker-proxy/testing";
const h = createJobsProxyHarness({
declaredJobs: [{ name: "rotate", kind: "cron" }],
});
h.client.registerCron("rotate", async () => doWork());
await h.flush();
await h.scheduler.tickCron("rotate");
h.close();
```
## Usage: Advanced
```ts
import { createJobsProxyHarness, runJobsConformance } from "@suaveplan/api-jobs-worker-proxy/testing";
await runJobsConformance(() =>
createJobsProxyHarness({
declaredJobs: [
{ name: "daily-rotate", kind: "cron" },
{ name: "send-email", kind: "queued" },
{ name: "ok", kind: "cron" },
],
handlerTimeoutMs: 5_000,
runRpcTimeoutMs: 5_000,
})
);
```
## API Reference
- `StubJobScheduler` — `registerCron`, `registerQueued`, `tickCron`,
`tickQueued` plus public `cron` / `queued` maps for inspection.
- `createJobsProxyHarness(opts) -> JobsProxyHarness` — full harness.
- `runJobsConformance(factory, throwOnFirstFailure = true)` — drives
the suite and reports per-case pass/fail.
- `JOBS_CONFORMANCE_CASES: ReadonlyArray<JobsConformanceCase>` — the
ordered scenario registry.
## Implementation Notes
`flush()` waits two macrotask turns: one for the worker-side
`registerJob` outbound to land on the main side, one for the main side's
`mainHandle.registerCron` call to settle before tests poke the
scheduler. The conformance scenarios assert via local `assertEq` and
`assertContains` helpers that throw plain `Error` instances; tests can
inspect `result.error` to discriminate between a scenario assertion and
a deeper failure (e.g. `JobsProxyError` raised during dispatch). The
`StubJobScheduler` deliberately exposes its internal `cron` / `queued`
maps so tests can verify that the round-trip actually reached the host
side, not just the wire.