147 lines
5.6 KiB
Markdown
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.
|