UNPKG

postgrejs

Version:

Professional PostgreSQL client NodeJS

233 lines (232 loc) 9.19 kB
import { Pool as LightningPool, } from 'lightning-pool'; import { coerceToBoolean, coerceToInt } from 'putil-varhelpers'; import { ConnectionState } from '../constants.js'; import { SafeEventEmitter } from '../safe-event-emitter.js'; import { getConnectionConfig } from '../util/connection-config.js'; import { Connection } from './connection.js'; import { getIntlConnection, IntlConnection } from './intl-connection.js'; export class Pool extends SafeEventEmitter { constructor(config) { super(); this._notificationListeners = new SafeEventEmitter(); const cfg = getConnectionConfig(config); this.config = Object.freeze(cfg); const poolOptions = {}; poolOptions.acquireMaxRetries = coerceToInt(cfg.acquireMaxRetries, 0); poolOptions.acquireRetryWait = coerceToInt(cfg.acquireRetryWait, 2000); poolOptions.acquireTimeoutMillis = coerceToInt(cfg.acquireTimeoutMillis, 0); poolOptions.idleTimeoutMillis = coerceToInt(cfg.idleTimeoutMillis, 30000); poolOptions.max = coerceToInt(cfg.max, 10); poolOptions.maxQueue = coerceToInt(cfg.maxQueue, 1000); poolOptions.max = coerceToInt(cfg.max, 10); poolOptions.min = coerceToInt(cfg.min, 0); poolOptions.minIdle = coerceToInt(cfg.minIdle, 0); poolOptions.validation = coerceToBoolean(cfg.validation, false); const poolFactory = { create: async () => { /* istanbul ignore next */ if (this.listenerCount('debug')) { this.emit('debug', { location: 'Pool.factory.create', pool: this, message: `new connection creating`, }); } const intlCon = new IntlConnection(cfg); await intlCon.connect(); intlCon.on('close', () => this._pool.destroy(intlCon)); /* istanbul ignore next */ if (this.listenerCount('debug')) { this.emit('debug', { location: 'Pool.factory.create', pool: this, message: `[${intlCon.processID}] connection created`, }); } return intlCon; }, destroy: intlCon => { /* istanbul ignore next */ if (this.listenerCount('debug')) { this.emit('debug', { location: 'Pool.factory.destroy', pool: this, message: `[${intlCon.processID}] connection destroy`, }); } return intlCon.close(); }, reset: async (intlCon) => { /* istanbul ignore next */ if (this.listenerCount('debug')) { this.emit('debug', { location: 'Pool.factory.reset', pool: this, message: `[${intlCon.processID}] connection reset`, }); } try { if (intlCon.state === ConnectionState.READY) { await intlCon.execute('ROLLBACK;UNLISTEN *'); } } finally { intlCon.removeAllListeners(); intlCon.once('close', () => this._pool.destroy(intlCon)); intlCon._refCount = 0; } }, validate: async (intlCon) => { /* istanbul ignore next */ if (this.listenerCount('debug')) { this.emit('debug', { location: 'Pool.factory.validate', pool: this, message: `[${intlCon.processID}] connection validate`, }); } if (intlCon.state !== ConnectionState.READY) throw new Error('Connection is not active'); await intlCon.execute('select 1;'); }, }; this._pool = new LightningPool(poolFactory, poolOptions); this._pool.on('return', (...args) => this.emit('release', ...args)); this._pool.on('error', (...args) => this.emit('error', ...args)); this._pool.on('acquire', (...args) => this.emit('acquire', ...args)); this._pool.on('destroy', (...args) => this.emit('destroy', ...args)); this._pool.start(); } /** * Returns number of connections that are currently acquired */ get acquiredConnections() { return this._pool.acquired; } /** * Returns number of unused connections in the pool */ get idleConnections() { return this._pool.available; } /** * Returns total number of connections in the pool regardless of whether they are idle or in use */ get totalConnections() { return this._pool.size; } /** * Obtains a connection from the connection pool */ async acquire() { const intlCon = await this._pool.acquire(); /* istanbul ignore next */ if (this.listenerCount('debug')) { this.emit('debug', { location: 'Pool.acquire', pool: this, message: `[${intlCon.processID}] acquired`, }); } const connection = new Connection(this, intlCon); /* istanbul ignore next */ if (this.listenerCount('debug')) connection.on('debug', (...args) => this.emit('debug', ...args)); if (this.listenerCount('execute')) connection.on('execute', (...args) => this.emit('execute', ...args)); if (this.listenerCount('query')) connection.on('query', (...args) => this.emit('query', ...args)); return connection; } /** * Shuts down the pool and destroys all resources. */ async close(terminateWait) { this._notificationListeners.removeAllListeners(); await this._notificationConnection?.close(terminateWait); const ms = terminateWait == null ? 10000 : terminateWait; return this._pool.closeAsync(ms); } /** * Executes a script */ async execute(sql, options) { const connection = await this.acquire(); try { return await connection.execute(sql, options); } finally { await this.release(connection); } } /** * Executes a query */ async query(sql, options) { const connection = await this.acquire(); try { return await connection.query(sql, options); } finally { await this.release(connection); } } async prepare(sql, options) { const connection = await this.acquire(); const statement = await connection.prepare(sql, options); statement.once('close', () => this._pool.release(getIntlConnection(connection))); return statement; } release(connection) { return this._pool.releaseAsync(getIntlConnection(connection)); } async listen(channel, callback) { if (!/^[A-Z]\w+$/i.test(channel)) throw new TypeError(`Invalid channel name`); this._notificationListeners.on(channel, callback); await this._initNotificationConnection(); } async unListen(channel) { if (!/^[A-Z]\w+$/i.test(channel)) throw new TypeError(`Invalid channel name`); this._notificationListeners.removeAllListeners(channel); if (!this._notificationListeners.eventNames().length) { await this.unListenAll(); } else if (this._notificationConnection) await this._notificationConnection.unListen(channel); } async unListenAll() { this._notificationListeners.removeAllListeners(); if (this._notificationConnection) { const conn = this._notificationConnection; this._notificationConnection = undefined; await conn.close(); } } async _initNotificationConnection() { if (this._notificationConnection) return; const conn = (this._notificationConnection = new Connection(this.config)); // Reconnect on connection lost conn.on('close', () => reConnect()); const registerEvents = async () => { const channels = this._notificationListeners.eventNames(); for (const channel of channels) { const fns = this._notificationListeners.listeners(channel); for (const fn of fns) { await conn.listen(channel, fn); } } }; const reConnect = async () => { setTimeout(() => { if (!this._notificationListeners.eventNames().length) return; conn.connect().catch(() => reConnect()); }, 500).unref(); }; await conn.connect(); await registerEvents(); } }