# 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: `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` 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.