# Capability Spec: `api-db-worker-proxy` **Package**: `@suaveplan/api-db-worker-proxy` **Status**: SHIPPED (2026-05-25, v0.1.0) > Every requirement carries a `**Verified by:**` line. ## Requirements ### Requirement: Admin and tenant-scoped surfaces are type-distinct The package SHALL expose two distinct worker-side surface types: `DbProxyClient` (extends `AdminDatabaseLike`) returned by `createDbProxyClient(port, { engine })`, and `TenantScopedDatabaseLike` returned by `wrapTenantScoped(adminProxy, tenantId)`. The two return types MUST NOT be mutually assignable: a tenant-scoped proxy MUST NOT satisfy an admin parameter and vice versa. Tenant scoping is enforced on the MAIN side; the worker-side facade is the type-level guarantee that the request slot has been narrowed to a tenant. #### Scenario: surfaces are not mutually assignable - **WHEN** `expectTypeOf().not.toMatchTypeOf()` is checked - **THEN** TypeScript flags the surfaces as distinct **Verified by:** `src/worker-side/worker-side.spec-conformance.test.ts::"DbProxyClient is NOT assignable to TenantScopedDatabaseLike"` AND `src/worker-side/worker-side.spec-conformance.test.ts::"wrapTenantScoped output is TenantScopedDatabaseLike, NOT AdminDatabaseLike"` --- ### Requirement: Cursors stream results without buffering The package SHALL marshal DB cursors as `AsyncIterable` via `@suaveplan/api-worker-rpc`'s `streamRequest` primitive. `for await (const row of cursor)` MUST yield rows incrementally; the main-side cursor MUST NOT be drained eagerly. Consumer cancellation via `AbortSignal` MUST propagate so the main-side cursor observes `signal.aborted` and stops producing rows. #### Scenario: large cursor yields incrementally - **WHEN** a worker iterates a 1000-row cursor - **THEN** every row is observed in order without preallocating the full result #### Scenario: cursor cancellation propagates - **WHEN** the worker aborts an `AbortController` mid-iteration - **THEN** the main-side cursor observes the abort and ceases yielding **Verified by:** `src/main-side/main-side.test.ts::"cursor streams rows in order"` AND `src/main-side/main-side.test.ts::"cursor cancellation aborts the main-side iterator"` AND `tests/conformance/run-db-adapter-conformance.test.ts::"cursor: 1000-row stream yields incrementally"` --- ### 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 MUST throw `DB_PROXY.TRANSACTION_LOST`. Auto-commit fires on callback resolve; auto-rollback fires on callback reject. Explicit `tx.commit()` or `tx.rollback()` inside the callback suppresses the auto-action. #### 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.TRANSACTION_LOST` is thrown #### Scenario: callback throw triggers rollback - **WHEN** the callback throws - **THEN** the worker-side wrapper invokes `tx.rollback` on the main side AND re-throws the original error **Verified by:** `src/worker-side/worker-side.test.ts::"tx handle leaked outside callback throws TRANSACTION_LOST"` AND `src/worker-side/worker-side.test.ts::"transaction auto-rolls-back when callback throws"` AND `src/worker-side/worker-side.test.ts::"auto-rollback path swallows rollback errors"` --- ### Requirement: Error namespace is the `DB_PROXY.*` registry The package SHALL expose a stable error-code registry `DB_PROXY_CODES` containing exactly four codes: `UNSAFE_NOT_PERMITTED`, `TRANSACTION_LOST`, `STREAM_INTERRUPTED`, `PROXY_DISPOSED`. Every public failure path MUST raise a `DbProxyError` carrying one of these codes; consumers branch on the literal value, not the message. #### Scenario: PROXY_DISPOSED after dispose - **WHEN** `dispose()` is called and a subsequent method is invoked - **THEN** the method throws `DbProxyError` with code `DB_PROXY.PROXY_DISPOSED` #### Scenario: UNSAFE_NOT_PERMITTED when adapter lacks capability - **WHEN** the underlying adapter does not implement `prepare` or `transaction` - **THEN** the proxy throws `DbProxyError` with code `DB_PROXY.UNSAFE_NOT_PERMITTED` **Verified by:** `src/error-codes/error-codes.test.ts::"exposes the four documented codes"` AND `src/worker-side/worker-side.test.ts::"dispose() rejects subsequent calls with PROXY_DISPOSED"` AND `src/main-side/main-side.test.ts::"rejects prepare when adapter does not implement it"` AND `src/main-side/main-side.test.ts::"rejects transaction when adapter does not implement it"` --- ### Requirement: Conformance harness backs Stage-4 adapter testing The package SHALL ship an in-process test harness (`createDbProxyHarness`) that pairs a `MessageChannel` with a `createDbProxyServer` on one port and a `createDbProxyClient` on the other, backed by an `InMemoryAdminDb` for deterministic conformance scenarios. Stage-4 DB adapters MUST pass conformance under BOTH the direct surface AND this proxy surface before Stage 4 can be declared complete. #### Scenario: harness drives a CRUD round-trip - **WHEN** a Stage-4 adapter test drives `createDbProxyHarness({ seed })` through INSERT / SELECT / DELETE - **THEN** every observation matches the direct-surface adapter **Verified by:** `tests/conformance/run-db-adapter-conformance.test.ts::"CRUD round-trip via the proxy"` AND `src/testing/testing.test.ts::"wires the in-memory db to a real proxy round-trip"`