UNPKG

pg-notify

Version:
185 lines (147 loc) 4.61 kB
const EventEmitter = require('node:events') const util = require('node:util') const pg = require('pg') const format = require('pg-format') const sjson = require('secure-json-parse') const sleep = util.promisify(setTimeout) const states = { init: 'init', closing: 'closing', reconnecting: 'reconnecting', connected: 'connected' } class PGPubSub { constructor (opts) { this.opts = opts this.ee = new EventEmitter() this.ee.setMaxListeners(0) this.reconnectMaxRetries = typeof opts.reconnectMaxRetries !== 'undefined' ? opts.reconnectMaxRetries : 10 this.maxPayloadSize = opts.maxPayloadSize || 7999 // default on a standard pg installation this.state = states.init this.reconnectRetries = 0 this.channels = {} /* c8 ignore next */ this.debug = process.env.DEBUG && process.env.DEBUG.includes('pg-notify') } async emit (channel, payload) { if (this.state !== states.connected) { throw new Error('[PGPubSub]: not connected') } if (typeof payload === 'object') { payload = JSON.stringify(payload) } const parsedPayload = format.literal(payload) if (Buffer.byteLength(parsedPayload, 'utf-8') > this.maxPayloadSize) { throw new Error(`[PGPubSub]: payload exceeds maximum size: ${this.maxPayloadSize}`) } this._debug('[emit] channel: ', channel) this._debug('[emit] payload: ', payload) this._debug('[emit] state: ', this.state) return this.client.query(`NOTIFY ${format.ident(channel)}, ${parsedPayload}`) } async on (channel, listener) { this._debug('[subscribe]', channel) if (this.state !== states.connected) { throw new Error('[PGPubSub]: not connected') } if (this.channels[channel]) { this.ee.on(channel, listener) this.channels[channel].listeners++ return } this.ee.on(channel, listener) this.channels[channel] = { listeners: 1 } return this.client.query(`LISTEN ${format.ident(channel)}`) } async removeListener (channel, listener) { if (!this.channels[channel]) { return } this.ee.removeListener(channel, listener) this.channels[channel].listeners-- if (this.channels[channel].listeners === 0) { delete this.channels[channel] return this.client.query(`UNLISTEN ${format.ident(channel)}`) } } async connect () { this.reconnectRetries = 0 this.state = states.init try { await this._setupClient() } catch (e) { await this._reconnect() } } async close () { if (this.state === states.closing) { return } this.state = states.closing this.channels = {} this.ee.removeAllListeners() if (this.client) { await this.client.end() } } async _reconnect (force) { this._debug('[_reconnect] state: ', this.state) if (!this.reconnectMaxRetries) { this.close() return } if ([states.reconnecting, states.closing].includes(this.state) && !force) { return } this.state = states.reconnecting this.reconnectRetries++ try { this.client.end() await this._setupClient() } catch (err) { if (this.reconnectRetries >= this.reconnectMaxRetries) { this.close() throw new Error('[PGPubSub]: max reconnect attempts reached, aborting', err) } if (![states.closing, states.connected].includes((this.state))) { await sleep(10) await this._reconnect(true) } } } async _setupClient () { this.client = new pg.Client(this.opts) await this.client.connect() this.client.on('notification', (message) => { this._debug('[_setupClient] notification', message) try { message.payload = sjson.parse(message.payload) } catch {} this.ee.emit(message.channel, message.payload) }) this.client.on('error', err => { this._debug('[_setupClient] error') if (this.reconnectRetries > this.reconnectMaxRetries) { this.close() throw new Error('[PGPubSub]: max reconnect attempts reached, aborting', err) } this._reconnect() }) this._debug('[_setupClient] init listeners') if (Object.keys(this.channels).length) { for (const channel in this.channels) { await this.client.query(`LISTEN ${format.ident(channel)}`) } } this.state = states.connected this.reconnectRetries = 0 this._debug('[_setupClient] init listeners done') } _debug (...args) { /* c8 ignore next 3 */ if (this.debug) { console.log(...args) } } } module.exports = PGPubSub