Files
travel-app/node_modules/bullmq/dist/esm/postgres/postgres-connection.js
2026-08-09 21:56:00 +00:00

208 lines
8.9 KiB
JavaScript

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();
}
}