first commit
This commit is contained in:
70
docs/api-stack/api/api-jobs-worker-proxy/README.md
Normal file
70
docs/api-stack/api/api-jobs-worker-proxy/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# @suaveplan/api-jobs-worker-proxy - Bidirectional MessagePort proxy that lets a worker-isolated API plugin register cron + queued jobs with the host's main-process job scheduler. Worker calls registerCron/registerQueued; main side stores handler refs and dispatches runJob RPC back to the worker when due. Universal. Stage-3 of the worker-isolation chain.
|
||||
|
||||
Bidirectional `MessagePort` proxy that lets a worker-isolated API plugin
|
||||
participate in the host's main-process job scheduler. The worker calls
|
||||
`registerCron` / `registerQueued` against a local handle implementing
|
||||
`JobRegistrationHandle` from `@suaveplan/api-types`; the proxy marshals
|
||||
the registration to the main side, which wires the closure into the
|
||||
real `JobRegistrationHandle` (typically backed by
|
||||
`@suaveplan/job-scheduler`). When the scheduler ticks a job, a
|
||||
`runJob` RPC flows back over the same port to invoke the worker's
|
||||
handler.
|
||||
|
||||
This is worker-isolation chain. The protocol layer
|
||||
(`@suaveplan/api-worker-rpc`) supplies correlation ids, abort
|
||||
forwarding, Zod-validated frames, and error reconstruction — this
|
||||
package adds only the jobs-specific verbs.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [client](./src/client/client.md) — `createJobsProxyClient(port, opts)` is the worker-tier entry point for the `@suaveplan/api-jobs-worker-proxy` package.…
|
||||
- [client](./src/client/client.md) — `createJobsProxyClient(port, opts)` is the worker-tier entry point for the `@suaveplan/api-jobs-worker-proxy` package.…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code registry for `@suaveplan/api-jobs-worker-proxy`. Every public failure path raise…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable error-code registry for `@suaveplan/api-jobs-worker-proxy`. Every public failure path raise…
|
||||
- [handler-bridge](./src/handler-bridge/handler-bridge.md) — This module owns the shared wire schemas, the worker-side `HandlerRegistry`, and the `runWithTimeout` helper used by bo…
|
||||
- [handler-bridge](./src/handler-bridge/handler-bridge.md) — This module owns the shared wire schemas, the worker-side `HandlerRegistry`, and the `runWithTimeout` helper used by bo…
|
||||
- [server](./src/server/server.md) — `createJobsProxyServer(port, mainHandle, opts)` runs in the host main process. It receives `registerJob` RPC frames fro…
|
||||
- [server](./src/server/server.md) — `createJobsProxyServer(port, mainHandle, opts)` runs in the host main process. It receives `registerJob` RPC frames fro…
|
||||
- [testing](./src/testing/testing.md) — The testing module exposes a fully-wired in-process harness so consumers can exercise the worker-proxy surface without…
|
||||
- [testing](./src/testing/testing.md) — The testing module exposes a fully-wired in-process harness so consumers can exercise the worker-proxy surface without…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Module map
|
||||
|
||||
| Subpath | File | Doc |
|
||||
|---|---|---|
|
||||
| `./worker-side` | [`src/client/client.ts`](./src/client/client.ts) | [client.md](./src/client/client.md) |
|
||||
| `./main-side` | [`src/server/server.ts`](./src/server/server.ts) | [server.md](./src/server/server.md) |
|
||||
| `./handler-bridge` | [`src/handler-bridge/handler-bridge.ts`](./src/handler-bridge/handler-bridge.ts) | [handler-bridge.md](./src/handler-bridge/handler-bridge.md) |
|
||||
| `./error-codes` | [`src/error-codes/error-codes.ts`](./src/error-codes/error-codes.ts) | [error-codes.md](./src/error-codes/error-codes.md) |
|
||||
| `./testing` | [`src/testing/testing.ts`](./src/testing/testing.ts) | [testing.md](./src/testing/testing.md) |
|
||||
|
||||
## Scope notes
|
||||
|
||||
- **Out of scope: tick delivery for non-jobs RPC.** Job ticks arrive
|
||||
via the `runJob` request envelope handled here; arbitrary request
|
||||
delivery for routes is the responsibility of the
|
||||
`api-router-worker-bridge`-style envelope path.
|
||||
- **Out of scope: cron expression parsing.** That is owned by
|
||||
`@suaveplan/cron` and runs on the main side.
|
||||
- **Out of scope: per-tenant fan-out.** Owned by
|
||||
`@suaveplan/job-scheduler` plus the `tenant.provider` capability.
|
||||
|
||||
## Local conformance harness
|
||||
|
||||
The proposal originally referenced `runJobsConformance` from
|
||||
`@suaveplan/job-scheduler/testing`. That harness does not yet exist;
|
||||
this package therefore provides its own
|
||||
`JOBS_CONFORMANCE_CASES` + `runJobsConformance` under `./testing`. The
|
||||
surface matches what the upstream harness is intended to be — a
|
||||
factory-driven scenario driver returning per-case pass/fail — so the
|
||||
in-repo substitute can be swapped out without changing consumers.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
bunx turbo lint typecheck test --filter=@suaveplan/api-jobs-worker-proxy
|
||||
bunx turbo test:coverage --filter=@suaveplan/api-jobs-worker-proxy
|
||||
```
|
||||
|
||||
@@ -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