Files
suaveplan/openspec/specs/api-jobs-worker-proxy/spec.md
2026-05-27 23:29:53 +01:00

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 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.