Files
2026-05-27 23:29:53 +01:00

147 lines
5.6 KiB
Markdown

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