/** * Connection pool for the graph-database backend. * * Design goals: * * 1. **Single-writer-multi-reader model.** One native `Database` per store * path, with a bounded fan-out of `Connection` objects on top of it. * Multiple `Connection`s from the same `Database` is the officially * supported concurrency pattern of the underlying native binding. * * 2. **One query per Connection at a time.** The native binding segfaults * when two `.query()` calls race against a single `Connection`. The * pool enforces this invariant structurally: every `query()` call * checks out a connection, runs exactly one statement, and checks it * back in. Queries compete for connections, never for a single * connection. * * 3. **Checkout queue with back-pressure.** When every connection is * busy, callers queue; waiters timeout at `WAITER_TIMEOUT_MS` so a * hung backend never leaks unbounded promises. * * 4. **Query timeout.** Each `query()` races against `QUERY_TIMEOUT_MS` * so a stuck query releases its slot even if the native call never * returns. Per-call `timeoutMs` overrides the default. * * 5. **Idle sweep + LRU eviction.** A single process-wide sweep runs * every `IDLE_SWEEP_INTERVAL_MS`, closing pools whose last use was * more than `IDLE_TIMEOUT_MS` ago and whose connections are all * idle. The LRU pathway evicts the least-recently-used pool when * the process-wide cap `MAX_POOL_SIZE` is reached. * * Native binding surface (`@ladybugdb/core@0.16.1`): * * - Global registry keys by the resolved `dbPath` and exposes * `GraphDbPool` as an instance object so `GraphDbStore.open()` / * `.close()` can drive the lifecycle without a second name registry. * - Timing knobs: `MAX_CONNS_PER_REPO=8`, waiter 15 s, query 30 s, * idle 60 s sweep + 5 min timeout, pool cap 5. * - Calls used here: `Database(path, bufferManagerSize, * enableCompression, readOnly)`, `new Connection(db)`, * `conn.query(stmt) → Promise`, * `result.getAll() → Promise[]>`. Prepared * statements use `conn.prepare(stmt)` + `conn.execute(stmt, params)`. */ import type { SqlParam } from "./interface.js"; /** * Structural shape of a native `Database`. Keeping the interface * statically typed (rather than reaching for `any`) lets tests inject a * fake by duck-typing. */ export interface NativeDatabase { close(): Promise; } /** * Structural shape of a native `Connection`. Typed to what the pool * actually calls — `query()` + `prepare()` + `execute()` + `close()`. */ export interface NativeConnection { query(stmt: string): Promise; prepare(stmt: string): Promise; execute(stmt: NativePreparedStatement, params?: Record): Promise; close(): Promise; } export interface NativeQueryResult { getAll(): Promise[]>; close?(): void; } export interface NativePreparedStatement { isSuccess(): boolean; getErrorMessage(): string; } /** * Structural shape of the `@ladybugdb/core` default export used by the * pool. Injected so tests can swap in fakes without loading the native * binding. */ export interface NativeBinding { Database: new (path: string, bufferManagerSize?: number, enableCompression?: boolean, readOnly?: boolean, maxDBSize?: number) => NativeDatabase; Connection: new (db: NativeDatabase) => NativeConnection; } export interface GraphDbPoolConfig { /** Max connections held per database file. Default 8. */ readonly maxConnections?: number; /** Global cap on number of distinct pools kept alive. Default 5. */ readonly maxPoolSize?: number; /** Milliseconds a checkout waiter can block before rejecting. Default 15000. */ readonly waiterTimeoutMs?: number; /** Default milliseconds a single query may run before aborting. Default 30000. */ readonly queryTimeoutMs?: number; /** Milliseconds of idleness before a pool is eligible for closure. Default 300000 (5 min). */ readonly idleTimeoutMs?: number; /** How often the idle sweep runs. Default 60000 (60 s). */ readonly idleSweepIntervalMs?: number; /** Open the database read-only. Default false. */ readonly readOnly?: boolean; /** * Buffer manager (temp-page region) size in bytes. lbug's native default is * `min(systemMemory, maxDBSize) * 0.8` — easily 50+ GiB on a beefy host. We * cap at 256 MiB by default so the in-memory page pool stays bounded across * many concurrent test DBs without affecting on-disk capacity. Production * callers can pass a larger value for analyze-time bulk loads. */ readonly bufferManagerBytes?: number; /** * Maximum on-disk database size (and the size of the per-Database virtual * mmap). lbug's native default is `1 << 43` = 8 TiB per Database, which * exhausts the 47-bit user virtual address space (~128 TiB) after ~16 * concurrent instances and surfaces as "Buffer manager exception: Mmap * for size 8796093022208 failed". Must be a power of 2. * * Default 16 GiB — comfortably larger than any plausible OCH graph * artifact (~few GiB at the high end), drops per-Database virtual reserve * 512×, and lets the test suite spin up hundreds of pools without * address-space pressure. */ readonly maxDbBytes?: number; /** * Injected native binding. Defaults to `require("@ladybugdb/core")` * via dynamic import on first `open()`. Tests inject a fake. */ readonly binding?: NativeBinding; } /** Defaults preserved from prior-art; changing these is a documented deviation. */ export declare const DEFAULT_MAX_CONNECTIONS = 8; export declare const DEFAULT_MAX_POOL_SIZE = 5; export declare const DEFAULT_WAITER_TIMEOUT_MS = 15000; export declare const DEFAULT_QUERY_TIMEOUT_MS = 30000; export declare const DEFAULT_IDLE_TIMEOUT_MS: number; export declare const DEFAULT_IDLE_SWEEP_INTERVAL_MS = 60000; /** * lbug `bufferManagerSize` cap. 2 GiB. Power of 2 not required. * * The buffer manager is the in-memory page cache; under-sizing it surfaces * as "Buffer manager exception: Unable to allocate memory! The buffer pool * is full and no memory could be freed!" the moment a single query's hot * working set exceeds the cap. lbug's native default is `min(systemMem, * maxDBSize) * 0.8` (≥50 GiB on a beefy host), so we cap explicitly to keep * concurrent test DBs from contending for physical RAM. 2 GiB is roughly * the largest BOM-body fixture × 4× headroom for vector ops. */ export declare const DEFAULT_BUFFER_MANAGER_BYTES: number; /** lbug `maxDBSize` cap. 16 GiB. MUST be a power of 2 — lbug enforces this. */ export declare const DEFAULT_MAX_DB_BYTES: number; interface Waiter { readonly resolve: (conn: NativeConnection) => void; readonly reject: (err: Error) => void; readonly timer: ReturnType; } /** * Process-wide registry. Keyed by resolved dbPath so parallel `GraphDbStore` * instances pointing at the same file share one native `Database` and a * single connection pool. Refcounted: the last `close()` against a shared * path tears the native resources down. */ interface RegistryEntry { readonly db: NativeDatabase; readonly connections: NativeConnection[]; readonly available: NativeConnection[]; readonly waiters: Waiter[]; readonly path: string; readonly config: ResolvedPoolConfig; refCount: number; checkedOut: number; lastUsed: number; closed: boolean; } type ResolvedPoolConfig = Required> & { binding?: NativeBinding; }; /** * Scan every registered pool and close those whose last use was more * than `idleTimeoutMs` ago with no outstanding checkouts. Exposed for * tests which inject a frozen clock. */ export declare function runIdleSweep(now?: number): void; /** * Pool handle. One instance per `GraphDbStore`; multiple instances over * the same path share the underlying native `Database` via the process * registry. */ export declare class GraphDbPool { private readonly path; private readonly config; private opened; private closed; constructor(path: string, config?: GraphDbPoolConfig); /** * Open (or re-use) the underlying `Database` and pre-warm connections. * Idempotent on the instance level. The registry refcount tracks * multiple stores over the same path. */ open(): Promise; /** * Release the pool's refcount. The underlying `Database` is torn down * only when the last holder closes. Idempotent. */ close(): Promise; /** * Execute a read-only statement. The pool checks out a connection, * runs the query under `timeoutMs`, and returns the parsed rows. */ query(stmt: string, params?: readonly SqlParam[], opts?: { readonly timeoutMs?: number; }): Promise[]>; /** * Execute a write statement that must bypass the Cypher read-only guard * — used exclusively by the internal bulk-load path for * `COPY FROM (UNWIND $rows ...)` calls. Not exposed on * `IGraphStore`; callers outside `GraphDbStore.bulkLoad` must NOT call * this method. */ execWrite(stmt: string, params?: Record, opts?: { readonly timeoutMs?: number; }): Promise; /** * Acquire a connection. Exposed for callers (e.g. bulk-load paths) * that need to hold a connection across multiple statements. * Remember to `release()` in `finally`. */ acquire(entry?: RegistryEntry): Promise; /** * Return a connection to the pool. If a waiter is queued, hand the * connection straight over rather than bouncing through `available`. */ release(entry: RegistryEntry, conn: NativeConnection): void; /** Inspect current queue sizes — used by tests and diagnostics. */ stats(): { available: number; checkedOut: number; waiters: number; refCount: number; }; isOpen(): boolean; private requireEntry; private runDirect; private runParameterized; } /** Number of live pools in the process-wide registry. */ export declare function _poolRegistrySize(): number; /** Force-close every pool and stop the sweep timer. Used in test teardown. */ export declare function _resetPoolRegistry(): void; export {}; //# sourceMappingURL=graphdb-pool.d.ts.map