Files
2026-05-27 23:29:53 +01:00

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 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"