Initial Commit
This commit is contained in:
146
openspec/specs/api-jobs-worker-proxy/spec.md
Normal file
146
openspec/specs/api-jobs-worker-proxy/spec.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user