postgrejs
Version:
Professional PostgreSQL client NodeJS
233 lines (232 loc) • 9.19 kB
JavaScript
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();
}
}