5.6 KiB
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>): void,
registerQueued(name, (payload: unknown) => Promise<void>): 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 acapabilities.JobRegistrationHandlevariable - 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 anonHandlerErrorobserver attached - THEN the observer fires with a
JobsProxyErrorwhosecodeisJOBS_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
JobsProxyErrorwhosecodeisJOBS_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
handlerTimeoutMsis 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: pairedcreateRawWorkerRpcClient+serveWorkerRpcon 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.