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

3.3 KiB

main-side

(no submodules)

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

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.

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.