5.5 KiB
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<DbProxyClient>().not.toMatchTypeOf<TenantScopedDatabaseLike>()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<Row> 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
AbortControllermid-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
txAND a method is called on it after the callback returns - THEN
DB_PROXY.TRANSACTION_LOSTis thrown
Scenario: callback throw triggers rollback
- WHEN the callback throws
- THEN the worker-side wrapper invokes
tx.rollbackon 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
DbProxyErrorwith codeDB_PROXY.PROXY_DISPOSED
Scenario: UNSAFE_NOT_PERMITTED when adapter lacks capability
- WHEN the underlying adapter does not implement
prepareortransaction - THEN the proxy throws
DbProxyErrorwith codeDB_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"