Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
# `main-side`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module exposes `createDbProxyServer(port, db)` — the trusted-side
endpoint of the worker DB proxy. It mounts an `AdminDatabaseLike`
adapter on a `MessagePort` and turns every worker-issued RPC frame
into a real call on the wrapped adapter. The main side is the only
side that touches the underlying DB driver; the worker code that
imported `@suaveplan/api-db-worker-proxy/worker-side` never sees a
real connection, never sees plaintext credentials, and cannot
fabricate a tenant identity.
The server keeps a registry of live transaction handles and prepared
statements keyed by id so the worker can drive them across multiple
round-trips without leaking driver handles into the worker's address
space.
## Features
- Marshals `query`, `execute`, `prepare`, `transaction`, `cursor`,
`engine`, and `close` over `@suaveplan/api-worker-rpc`.
- Streams cursor rows as `stream-chunk` frames; worker-side break
triggers `cancel` which the handler observes via its `AbortSignal`.
- Bounds transaction lifetimes by port: closing the server (or
receiving `goodbye`) raises `TRANSACTION_LOST` on every dangling
handle rather than leaking driver state.
- Exposes diagnostic counters `activeTransactionCount` and
`activePreparedCount`.
## Usage: Basic
```ts
import { createDbProxyServer } from "@suaveplan/api-db-worker-proxy/main-side";
import { Worker, MessageChannel } from "node:worker_threads";
const channel = new MessageChannel();
const server = createDbProxyServer(channel.port1, realAdapter);
const worker = new Worker("./worker.js");
worker.postMessage({ port: channel.port2 }, [channel.port2]);
```
## Usage: Advanced
When the host needs to scope the adapter per request, pass an
already-tenant-scoped adapter to a per-request server. The proxy
itself does NOT rewrite SQL — that responsibility lives in the
adapter the host hands in here. `wrapTenantScoped` on the worker
side is purely a type-level facade.
```ts
const scoped = realAdapter.scopedFor(tenantId); // host responsibility
const server = createDbProxyServer(port, scoped);
// shut down at request end:
server.close("request finished");
```
## API Reference
- `createDbProxyServer(port: PortLike, db: AdminDatabaseLike): DbProxyServer`.
- `DbProxyServer.close(reason?: string): void` — closes the server,
rolling back live transactions and finalising prepared statements.
- `DbProxyServer.activeTransactionCount(): number`.
- `DbProxyServer.activePreparedCount(): number`.
## Implementation Notes
The cursor handler returns an `AsyncGenerator`; `serveWorkerRpc`'s
streaming path interprets the return as `AsyncIterable<unknown>` and
emits one `stream-chunk` per yield, then `stream-end ok`. Cancellation
from the worker comes in as an `AbortSignal.aborted = true` on the
handler's `signal`; the generator throws `STREAM_INTERRUPTED` which
the server marshals as `stream-end ok=false` carrying the
`DB_PROXY.STREAM_INTERRUPTED` code.
Transactions are implemented by holding the adapter's `transaction()`
promise open until either `tx.commit` or `tx.rollback` fires from the
worker. Rolling back is signalled by throwing inside the adapter
callback, which mirrors the adapter's own rollback semantics without
adding a parallel state machine.