/** * Worker-side helpers for the generic workload protocol whose main-thread * half is `WorkloadHost` (`./host.ts`). * * Call {@link installWorkloadHandler} once at the top of a dedicated * worker module. Domain code only implements `run`. */ import type { WorkloadRequest } from "./protocol"; /** What a job implementation gets besides its payload. */ export interface WorkloadWorkerContext { /** Correlated job id from the host. */ id: number; /** * True after the host sent `{ type: "cancel", id }` for this job. A flag to * poll, not a signal that interrupts — nothing stops until the job checks it. */ isCancelled: () => boolean; /** * Stream progress (any domain shape) to the host as * `{ type: "progress", id, progress }`. Also arms the heartbeat: see * {@link WorkloadHandlerOptions.heartbeatMs}. */ reportProgress: (progress: unknown) => void; /** * Ask the host to do something on the worker's behalf and await its answer. * * Posts `{ type: "host-call", callId, call }` (with `transfer` forwarded to * `postMessage`, so byte payloads move zero-copy) and resolves with the * host's `result` when the matching `{ type: "host-reply", callId, ok: true }` * arrives. An `ok: false` reply rejects with * `[workload] host call : `. The `call` payload is opaque to * this layer — its shape is a domain contract with the host's `onHostCall`. * * `callId` is auto-incremented per worker realm and independent of job ids; * pending calls are shared across concurrently interleaved jobs and die with * the worker (host `dispose()` needs no cross-realm cleanup). */ callHost: (call: unknown, transfer?: Transferable[]) => Promise; } /** * The slice of `DedicatedWorkerGlobalScope` that * {@link installWorkloadHandler} touches. * * Exists so a unit test can drive the handler with a plain object instead of a * real worker global — production callers pass nothing and get `self`. */ export interface WorkloadWorkerScope { onmessage: ((ev: MessageEvent>) => void) | null; postMessage(msg: unknown, transfer?: Transferable[]): void; } /** Domain hooks for {@link installWorkloadHandler}. */ export interface WorkloadHandlerOptions { /** * Execute one job. May be async; cooperative cancel via * `ctx.isCancelled()` between chunks. A thrown error is reported to the host * as `{ type: "error" }` — it never becomes an unhandled rejection. * * Buffers returned in `transfer` are transferred with the `done` message * (and detached inside the worker), so return every result array that the * worker no longer needs. */ run: (job: TJob, ctx: WorkloadWorkerContext) => Promise<{ result: TResult; transfer?: Transferable[]; }>; /** * Re-post the **last** progress payload this often while a job runs, so a * host-side stall watch does not fire during a long silent stretch. * * The heartbeat repeats real progress and never invents a percentage, so no * beat is sent until the job has called `ctx.reportProgress` at least once. * And, like any worker timer, a beat can only land when the job yields to the * worker event loop: a fully synchronous WebAssembly (WASM) call blocks the heartbeat as * well, and the pending beat arrives once that call returns. * * Default 8 s; set 0 to disable. */ heartbeatMs?: number; /** * How run requests share the single worker realm. Default `"fifo"`. * * - `"fifo"` — strictly serial: requests queue in arrival order and a job's * `run` is not invoked until the previous job posted `done` or `error`. * One heavy WASM job per realm, which keeps two big * allocations from meeting in the same heap. * - `"interleaved"` — every request starts immediately; jobs cooperatively * interleave at their own `await` boundaries (e.g. a `ctx.callHost` * round-trip). This is event-loop interleaving, **not** parallelism: the * realm still has one thread and one WASM heap, so a fully synchronous * stretch in one job blocks all others until it yields. Use it when a * long streaming job must not starve short requests; spawn another worker * for real parallelism. * * Cancel semantics are identical in both modes: the flag is remembered * until the job it names settles, so a queued fifo job sees it on its first * poll and an interleaved job should check `ctx.isCancelled()` at its start. */ scheduling?: "fifo" | "interleaved"; } /** * Install the standard run/cancel/ready loop on the worker global, then post * `{ type: "ready" }` so the host's `whenReady()` resolves. * * Call it exactly once per worker module: it assigns `scope.onmessage`, so a * second call replaces the first handler. * * **Scheduling is `"fifo"` by default** — first-in-first-out, one job at a * time — and can be switched to `"interleaved"` per worker module; see * {@link WorkloadHandlerOptions.scheduling} for the trade-off (single WASM * heap, cooperative interleaving, not parallelism). * * **Cancel is per job, and never too early.** A `cancel` is remembered for its * id whether the job is running, still queued, or the id is unknown, so * `ctx.isCancelled()` is already true on a queued job's first poll and it can * skip the work entirely. The flag is dropped when that job settles — in * either scheduling mode. * * **The worker can call back.** Each job's `ctx.callHost` opens a * `host-call`/`host-reply` round-trip answered by the host's `onHostCall`; * a `host-reply` whose `callId` matches no pending call is silently ignored. * * @param options the domain `run` implementation, plus the heartbeat interval * and scheduling mode * @param scope where to install, defaulting to the worker global. It exists for * unit tests, which pass a plain {@link WorkloadWorkerScope} so the loop runs * with no real worker; production worker modules omit it. */ export declare function installWorkloadHandler(options: WorkloadHandlerOptions, scope?: WorkloadWorkerScope): void;