# Capability Spec Delta: api-db-worker-proxy (api-db-worker-proxy) **Package**: `@suaveplan/api-db-worker-proxy` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. ## ADDED Requirements ### Requirement: Admin and tenant-scoped surfaces are type-distinct proxy clients The package SHALL expose `createDbProxyClient(port, { surface })` whose return type is `WorkerProxyOf` when `surface === "admin"` and `WorkerProxyOf` when `surface === "tenant-scoped"`. The two return types MUST NOT be mutually assignable: a tenant-scoped proxy MUST NOT satisfy an admin parameter and vice versa. #### Scenario: surfaces are not mutually assignable - **WHEN** `expectTypeOf().not.toEqualTypeOf()` is checked - **THEN** TypeScript flags the surfaces as distinct **Verified by:** `src/client/client.spec-conformance.test.ts::"admin and tenant-scoped surfaces are distinct"` --- ### Requirement: Cursors stream results without buffering The package SHALL marshal DB cursors as `ReadableStream` via `api-worker-rpc`'s streaming primitive. `for await (const row of cursor)` MUST yield rows incrementally; the main-side cursor MUST NOT be drained eagerly. Consumer cancellation MUST propagate so the main-side cursor closes. #### Scenario: 10k-row cursor yields incrementally - **WHEN** a worker iterates a 10k-row cursor - **THEN** the first row is observed BEFORE the last row has been produced main-side (verified via timing) #### Scenario: cursor break propagates close - **WHEN** the worker breaks out of `for await` early - **THEN** the main-side cursor is closed AND no further rows are produced **Verified by:** `src/cursor/cursor.test.ts::"cursor yields incrementally"` AND `src/cursor/cursor.test.ts::"break propagates cursor close"` --- ### Requirement: Transaction handle is scoped to callback lifetime The package SHALL provide `client.transaction(async (tx) => { ... })` where `tx` is only valid inside the callback. A call on `tx` after the callback resolves or rejects MUST throw `DB_PROXY_TX_OUT_OF_SCOPE`. The transaction MUST be carried by a transaction-scoped sub-port so the main side can correlate every call to the same connection. #### Scenario: out-of-scope tx call throws - **WHEN** the callback captures `tx` AND a method is called on it after the callback returns - **THEN** `DB_PROXY_TX_OUT_OF_SCOPE` is thrown #### Scenario: nested tx is rejected - **WHEN** `tx.transaction(...)` is invoked inside the callback - **THEN** `DB_PROXY_TX_OUT_OF_SCOPE` is thrown (savepoints are out of scope for Stage 3) **Verified by:** `src/transaction/transaction.test.ts::"out-of-scope tx call throws"` AND `src/transaction/transaction.test.ts::"nested tx is rejected"` --- ### Requirement: Conformance suite passes against this proxy The package SHALL be runnable as a surface argument to `runDbAdapterConformance` from `@suaveplan/api-db/testing`. Every conformance scenario (CRUD, transactions, cursors, schema introspection) MUST pass identically to a direct in-process adapter. This is the Stage-4-precondition gate: every Stage-4 DB adapter (`api-db-adapter`, `api-db-sqlite-memory`) MUST pass conformance under BOTH the direct surface AND this worker-proxy surface before Stage 4 can be declared complete. #### Scenario: runDbAdapterConformance passes - **WHEN** `runDbAdapterConformance("worker-proxy", makeFactory)` runs against this proxy + stub server - **THEN** every conformance scenario passes **Verified by:** `tests/conformance/run-db-adapter-conformance.test.ts::"runDbAdapterConformance passes against proxy"`