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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user