Files
suaveplan/openspec/archive/2026-05-25-api-db-worker-proxy/specs/api-db-worker-proxy/spec.md
2026-05-27 23:29:53 +01:00

3.6 KiB

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<AdminDatabaseLike> when surface === "admin" and WorkerProxyOf<TenantScopedDatabaseLike> 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<Admin>().not.toEqualTypeOf<TenantScoped>() 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<Row> 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"