# Capability: api-jobs-worker-proxy **Package**: `@suaveplan/api-jobs-worker-proxy` **Status**: SHIPPED · 2026-05-25 The `@suaveplan/api-jobs-worker-proxy` package provides the worker ↔ main bridge that lets a worker-isolated API plugin participate in the host's main-process job scheduler. Worker code calls `registerCron` / `registerQueued` against the local proxy; the proxy marshals the registration to main via `@suaveplan/api-worker-rpc`, where it is bound into the host's real `JobRegistrationHandle`. When the host scheduler ticks, a `runJob` RPC flows back to the worker and invokes the registered handler closure. ## Requirements ### Requirement: Proxy preserves JobRegistrationHandle surface The package SHALL expose `createJobsProxyClient(port)` returning an object implementing `capabilities.JobRegistrationHandle` from `@suaveplan/api-types`. Method names + parameter shapes MUST match the direct surface (`registerCron(name, () => Promise): void`, `registerQueued(name, (payload: unknown) => Promise): void`); the proxy adds only `close(reason?)` and a `sizes` accessor. #### Scenario: surface matches JobRegistrationHandle - **WHEN** a TypeScript consumer assigns the result of `createJobsProxyClient(port)` to a `capabilities.JobRegistrationHandle` variable - **THEN** the assignment type-checks without widening **Verified by:** `src/client/client.test.ts::"registerCron round-trips, scheduler tick fires handler"` plus typecheck of `src/client/client.ts`. --- ### Requirement: Non-manifested job name is rejected The package SHALL surface main-side rejection of non-manifested job names as `JOBS_PROXY.JOB_NOT_DECLARED`. The host enforces — declared in the plugin's manifest `jobs[]` array, supplied as `declaredJobs` — and rejects unknown names at the boundary. #### Scenario: unknown job name rejects - **WHEN** the worker calls `ctx.jobs.registerCron("unknown-job", fn)` with an `onHandlerError` observer attached - **THEN** the observer fires with a `JobsProxyError` whose `code` is `JOBS_PROXY.JOB_NOT_DECLARED` **Verified by:** `src/client/client.test.ts::"non-declared job name triggers JOB_NOT_DECLARED via onHandlerError"` --- ### Requirement: Kind mismatch rejects The package SHALL reject registrations whose kind (`cron` vs `queued`) does not match the manifest declaration with the same `JOBS_PROXY.JOB_NOT_DECLARED` code, since the name-to-kind binding is a manifest invariant. #### Scenario: cron registered as queued (or vice versa) rejects - **WHEN** the worker registers a queued-declared name as cron - **THEN** the rejection observer fires with `JOB_NOT_DECLARED` **Verified by:** `src/client/client.test.ts::"declared queued name but registered as cron is rejected"` --- ### Requirement: Worker handler exceptions surface back to scheduler When the main-side scheduler ticks a job and the worker-side handler throws or rejects, the proxy SHALL propagate the failure to the scheduler as a `JobsProxyError` of code `JOBS_PROXY.HANDLER_THREW`. The worker-side observer (if any) also receives the same error. #### Scenario: handler throws → scheduler tick rejects - **WHEN** the worker registers a cron handler that throws and the scheduler ticks that name - **THEN** the tick promise rejects with a `JobsProxyError` whose `code` is `JOBS_PROXY.HANDLER_THREW` **Verified by:** `src/client/client.test.ts::"worker handler that throws surfaces via onHandlerError"` --- ### Requirement: Per-handler timeout is honored The proxy SHALL apply an optional per-handler timeout (`handlerTimeoutMs`, default `0` = disabled). When a handler exceeds the budget the proxy SHALL surface `JOBS_PROXY.JOB_TIMEOUT` to both the scheduler and the `onHandlerError` observer. #### Scenario: long-running handler exceeds budget - **WHEN** `handlerTimeoutMs` is 5 and the worker handler sleeps 200ms, then the scheduler ticks - **THEN** the tick promise rejects and the observer receives `JOBS_PROXY.JOB_TIMEOUT` **Verified by:** `src/client/client.test.ts::"handlerTimeoutMs aborts long-running handlers"` --- ### Requirement: Conformance suite passes against this proxy The package SHALL ship a conformance suite (`JOBS_CONFORMANCE_CASES` + `runJobsConformance(factory)`) covering cron round-trip, queued round-trip with payload, and rejection of non-declared names. The suite MUST pass against the in-process harness factory. #### Scenario: runJobsConformance passes - **WHEN** `runJobsConformance(factory)` runs against the standard in-process harness - **THEN** every scenario reports `ok: true` **Verified by:** `tests/conformance/run-jobs-conformance.test.ts::"every scenario reports ok against the standard harness factory"` --- ## Implementation summary - Layout: `packages/api-stack/api/api-jobs-worker-proxy/` - Subpath exports: `./worker-side`, `./main-side`, `./handler-bridge`, `./error-codes`, `./testing` - Deps: `@suaveplan/api-types`, `@suaveplan/api-worker-rpc`, `@suaveplan/error`, `tslib`, `zod` - Codes (5): `JOB_NOT_DECLARED`, `HANDLER_THREW`, `PROXY_DISPOSED`, `JOB_TIMEOUT`, `DUPLICATE_REGISTRATION` - 44 tests / 100/100/100/100 statement/branch/function/line coverage - Bidirectional over a single `MessagePort`: paired `createRawWorkerRpcClient` + `serveWorkerRpc` on each side. ## Local substitute for `runJobsConformance` The proposal referenced `@suaveplan/job-scheduler/testing::runJobsConformance`. That harness does not yet exist on genesis. The local `runJobsConformance` shipped under `./testing` is a forward-compatible substitute: it takes the same factory shape and returns per-case pass/fail, so consumers can later swap to the upstream harness with no public-surface change.