import { __rest } from "tslib"; import { EventEmitter } from 'events'; import { isPgPool, } from './pg-types'; import { DEFAULT_SCHEMA, quoteSchemaName, runMigrations } from './migrator'; /** * Lazily loads the optional `pg` (node-postgres) driver. Redis-only users never * hit this path, so they never need `pg` installed. * * Only reached when the caller passes a config/connection string (not an * already-constructed `pg.Pool`). In native ESM environments where `require` is * unavailable, callers should pass a `pg.Pool` instance instead. */ function loadPgModule() { try { if (typeof require === 'function') { return require('pg'); } } catch (_a) { // Fall through to the friendly error below. } throw new Error("The PostgreSQL backend could not load the optional 'pg' package. " + 'Install it with `npm install pg`. In a native ESM environment, pass an ' + 'already-constructed `pg.Pool` instance as the connection instead of a ' + 'config object or connection string.'); } /** * Owns the PostgreSQL connection resources for a single backend: * * - a `pg.Pool` for regular, short-lived queries, and * - a dedicated, long-lived `LISTEN` client used by the blocking * "wait for job" primitive (lazily established). * * Lifecycle mirrors {@link RedisConnection}: it is an {@link EventEmitter} that * surfaces normalized `'ready' | 'error' | 'close'` events, exposes * {@link PostgresConnection.waitUntilReady} (which also runs the schema * migrations exactly once, on a dedicated checked-out client) and * {@link PostgresConnection.close}. */ export class PostgresConnection extends EventEmitter { constructor(connection) { super(); /** * `true` when {@link listenClient} is a standalone `pg.Client` we must `end()` * (owned pool); `false` when it is a pooled client we must `release()`. */ this.listenClientIsStandalone = false; if (isPgPool(connection)) { this.pool = connection; this.ownsPool = false; this.schema = DEFAULT_SCHEMA; this.skipVersionCheck = false; this.pgModule = undefined; this.listenClientConfig = undefined; } else { const pg = loadPgModule(); const _a = typeof connection === 'string' ? { schema: undefined, skipVersionCheck: undefined, connectionString: connection, } : connection, { schema, skipVersionCheck } = _a, poolConfig = __rest(_a, ["schema", "skipVersionCheck"]); this.schema = schema !== null && schema !== void 0 ? schema : DEFAULT_SCHEMA; this.skipVersionCheck = skipVersionCheck !== null && skipVersionCheck !== void 0 ? skipVersionCheck : false; // Validate early so a bad schema name fails fast (and before any DDL). const quotedSchema = quoteSchemaName(this.schema); // Pin every pooled connection's search_path to the schema so the `.sql` // command files use unqualified, portable names. Quoted to match the // migration's quoted CREATE SCHEMA (case-preserving). const searchPathOption = `-c search_path=${quotedSchema}`; const existingOptions = poolConfig.options; const resolvedConfig = Object.assign(Object.assign({}, poolConfig), { options: existingOptions ? `${existingOptions} ${searchPathOption}` : searchPathOption }); this.pool = new pg.Pool(resolvedConfig); this.ownsPool = true; // Keep the means to build a dedicated LISTEN connection on demand. this.pgModule = pg; this.listenClientConfig = resolvedConfig; } // The pool emits 'error' for idle clients that drop; surface it but never // let it crash the process — hence the guarded {@link emitError} (a bare // `emit('error')` with no listeners throws). this.pool.on('error', err => this.emitError(err)); } /** * Forwards an underlying pool / LISTEN-client error as this connection's * `'error'` event, but only when a listener is attached. `EventEmitter.emit` * throws when emitting `'error'` with no listeners, so an unguarded forward * would turn an idle-client error into a hard process crash. Mirrors the * guard in {@link RedisConnection}. */ emitError(err) { if (this.listenerCount('error') > 0) { this.emit('error', err); } } /** * Resolves once the pool is reachable and the schema is up to date. * * Idempotent and memoized: the migration runs exactly once per connection, * on a single dedicated client checked out of the pool (so the migration's * advisory lock and transaction share one session — see {@link runMigrations}). */ async waitUntilReady() { if (!this.readyPromise) { this.readyPromise = this.bootstrap(); } return this.readyPromise; } async bootstrap() { const client = await this.pool.connect(); try { await runMigrations(client, this.schema, { skipVersionCheck: this.skipVersionCheck, }); } finally { client.release(); } // Defer so listeners attached synchronously after construction still fire. setTimeout(() => this.emit('ready'), 0); } /** * Returns the dedicated client used for `LISTEN`/`NOTIFY`, establishing it on * first use. * * When this connection owns the pool we use a *standalone* `pg.Client` (its * own dedicated TCP connection) so the long-lived `LISTEN` never consumes a * pool slot — this is what lets the query pool run at `max: 1` without * deadlocking. When the user supplied their own `pg.Pool` we check a client * out of it (so such pools should be sized `>= 2`). */ async getListenClient() { // Memoize the establishment promise (not just the resolved client) so two // callers racing on the first use don't each open a connection — the // `await` below is exactly where a second caller would otherwise slip in. if (!this.listenClientPromise) { this.listenClientPromise = (async () => { if (this.pgModule && this.listenClientConfig) { const client = new this.pgModule.Client(this.listenClientConfig); await client.connect(); client.on('error', err => this.emitError(err)); this.listenClientIsStandalone = true; this.listenClient = client; return client; } else { const client = await this.pool.connect(); client.on('error', err => this.emitError(err)); this.listenClientIsStandalone = false; this.listenClient = client; return client; } })(); } return this.listenClientPromise; } /** * Truthy once {@link PostgresConnection.close} has begun. */ get isClosing() { return this.closing; } /** * Closes the connection: releases the `LISTEN` client and (if owned) ends the * pool. Safe to call multiple times. */ async close() { if (!this.closing) { this.closing = (async () => { var _a, _b; // Await any in-flight establishment so we never leak a LISTEN client // whose `getListenClient` promise was still pending when close() ran. const client = (_a = this.listenClient) !== null && _a !== void 0 ? _a : (await ((_b = this.listenClientPromise) === null || _b === void 0 ? void 0 : _b.catch(() => undefined))); this.listenClientPromise = undefined; if (client) { client.removeAllListeners(); if (this.listenClientIsStandalone) { // Standalone dedicated connection: end it outright. await client.end(); } else { // Pooled client: return it to the pool. client.release(); } this.listenClient = undefined; } if (this.ownsPool) { await this.pool.end(); } this.emit('close'); })(); } return this.closing; } /** * Forcibly tears down the connection. For PostgreSQL there is no distinct * "disconnect without waiting" semantics beyond closing, so this delegates to * {@link PostgresConnection.close}. */ async disconnect() { return this.close(); } }