3.3 KiB
Main-Side Jobs Proxy Server
(no submodules)
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:
declaredJobsis the authoritative whitelist. Names not in the list reject withJOBS_PROXY.JOB_NOT_DECLARED. - Kind enforcement: registering a name as
cronwhen the manifest declares itqueued(or vice versa) is also rejected asJOB_NOT_DECLARED. - Optional per-dispatch RPC timeout (
runRpcTimeoutMs) so a hung worker handler does not stall scheduler ticks indefinitely. onDispatchErrorobserver for telemetry on failed worker invocations.- Idempotent
close()tears down the inbound listener AND the outbound RPC client. registeredCountaccessor for tests + diagnostics.
Usage: Basic
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
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.