4.1 KiB
@suaveplan/api-db-worker-proxy — worker-side proxy to main-process DB adapter
Package: @suaveplan/api-db-worker-proxy
Status: PROPOSED
Type: new-package
Stage: 3 (Worker isolation infrastructure)
Why
Per design doc §5.2a, every capability ships two callable surfaces. db is special because (a) its operations return result sets that can be huge (favoring streaming over buffering), (b) its operations are transactional (transaction handles must be tied to a single connection lifetime that the worker can't naively shuttle around), and (c) tenant-scoping is a security boundary the worker MUST NOT be able to circumvent. api-db-worker-proxy provides the worker-side surface of AdminDatabaseLike / TenantScopedDatabaseLike whose method calls marshal across MessagePort to the main-process DB adapter (which lives in api-db-adapter, Stage 4).
The proxy preserves the PluginContext / RequestContext db-slot type contract: a PluginContext's db is AdminDatabaseLike; a RequestContext's db is TenantScopedDatabaseLike. Tenant scoping is enforced on the MAIN side before the call is dispatched — worker code cannot smuggle an admin DB into a request handler.
What changes
Net-new package. Worker-side only — the main-side counterpart lives in api-db-adapter (Stage 4).
ADDED exports:
./client—createDbProxyClient(port, { surface: "admin" | "tenant-scoped" }): returns aWorkerProxyOf<AdminDatabaseLike>orWorkerProxyOf<TenantScopedDatabaseLike>typed per surface../cursor— DB cursors marshalled asReadableStream<Row>viaapi-worker-rpc's streaming primitive. Worker-sidefor await (const row of cursor)is identical to direct usage../transaction— transaction handle proxy:client.transaction(async (tx) => { ... })opens a transaction on the main side; the worker-side callback gets atxwhose methods route through a transaction-scoped sub-port. Calls outside the callback throwDB_PROXY_TX_OUT_OF_SCOPE../error-codes—DB_PROXY_*codes (DB_PROXY_PORT_CLOSED,DB_PROXY_TX_OUT_OF_SCOPE,DB_PROXY_CURSOR_AFTER_CLOSE,DB_PROXY_SURFACE_MISMATCH)../testing— pairedMessageChannel+ a stub main-side server for unit tests; conformance harnessrunDbProxyClientConformancefor downstream adapters.
Out of scope
- Main-side adapter implementation —
api-db-adapter(Stage 4) andapi-db-sqlite-memory(Stage 5) implement the receiving side. - Tenant scoping enforcement — enforced on the main side BEFORE marshalling. This package is the transport, not the policy.
- Engine-specific SQL —
api-db-worker-proxyis engine-agnostic; the main-side adapter resolves the engine (postgres/sqlite/surrealdb). - Schema migrations — host metadata migrations live in
api-host; plugin migrations live inapi-db-adapter.
Source mapping
Genesis-original. Design doc sections: §5.1 (two-context separation of db slot), §5.2a (worker-RPC variants), §6 (capability scoping in step 7).
| Requirement | Design doc section |
|---|---|
Two surfaces: AdminDatabaseLike vs TenantScopedDatabaseLike |
§5.1 |
Cursors streamed via Transferable |
§5.2a |
| Worker-side proxy preserves direct-surface ergonomics | §5.2a |
| Tenant scoping enforced main-side, not worker-side | §6 |
Verification approach
src/client/client.spec-conformance.test.ts—expectTypeOfproves admin surface and tenant surface are distinct; admin surface NOT assignable to tenant-scoped.src/cursor/cursor.test.ts—for awaitover large result set; consumer cancel propagates to main side.src/transaction/transaction.test.ts—txonly valid inside callback; out-of-scope call throwsDB_PROXY_TX_OUT_OF_SCOPE.tests/conformance/run-db-adapter-conformance.test.ts—runDbAdapterConformance("worker-proxy", makeFactory)from@suaveplan/api-dbpasses against this proxy + a stub main-side server. Required before Stage 4 can complete — every Stage-4 DB adapter MUST pass conformance under BOTH the direct and worker-proxy surfaces.
Migration impact
None — additive only.