212 lines
9.1 KiB
JavaScript
212 lines
9.1 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.PostgresConnection = void 0;
|
|
const tslib_1 = require("tslib");
|
|
const events_1 = require("events");
|
|
const pg_types_1 = require("./pg-types");
|
|
const migrator_1 = require("./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}.
|
|
*/
|
|
class PostgresConnection extends events_1.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 ((0, pg_types_1.isPgPool)(connection)) {
|
|
this.pool = connection;
|
|
this.ownsPool = false;
|
|
this.schema = migrator_1.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 = tslib_1.__rest(_a, ["schema", "skipVersionCheck"]);
|
|
this.schema = schema !== null && schema !== void 0 ? schema : migrator_1.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 = (0, migrator_1.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 (0, migrator_1.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();
|
|
}
|
|
}
|
|
exports.PostgresConnection = PostgresConnection;
|