UNPKG

pg-promise-listener

Version:

Resilient notification listener for Postgres. Based on code from pg-promise module

155 lines (136 loc) 4.9 kB
//Original inspiration: //https://github.com/vitaly-t/pg-promise/wiki/Robust-Listeners //Self-ping each second to check const PING_INTERVAL = 1000; //Any message with this string will be thrown away. //We are using a guid to ensure that we won't clash with an actual messages. const PING_MESSAGE = '0a7735a0-93b6-4830-835b-72d0f552381c'; const SELF_CHECK_MESSAGE = 'f75976d0-dbb6-441f-b62a-264dc689d933'; //Default: retry 10 times, with 5-second intervals const DEFAULT_RETRY_COUNT = 10; const DEFAULT_RETRY_INTERVAL = 5000; const DEFAULT_SELF_CHECK_TIMEOUT = 20000; module.exports = DatabaseListener = function({ dbConnection, //pg-promise-connection to your database onDatabaseNotification, //callback for message handling channel, //name of your channel, i.e. the channel name with NOTIFY in your database logger = null, //If you don't like console.log, insert your own logger parseJson = false, //Can your notify-messages be parsed from json? maxRetryCount = DEFAULT_RETRY_COUNT, retryInterval = DEFAULT_RETRY_INTERVAL, selfCheckTimeout = DEFAULT_SELF_CHECK_TIMEOUT, }) { if (!dbConnection) throw new Error('DatabaseListener: Missing dbConnection'); if (!channel) throw new Error('DatabaseListener: Missing channel name'); this.logger = logger || console.log; this.db = dbConnection; this.channel = channel; this.onDatabaseNotification = onDatabaseNotification; this.parseJson = parseJson; this.maxRetryCount = maxRetryCount; this.retryInterval = retryInterval; this.selfCheckTimeout = selfCheckTimeout; // global connection for permanent event listeners this.connection = null; this.selfCheck = () => { return new Promise((resolve, reject) => { this.selfCheckCallback = () => resolve(true); setTimeout(() => resolve(false), this.selfCheckTimeout); this.connection .none('NOTIFY $1~, $2', [this.channel, SELF_CHECK_MESSAGE]) .catch(error => resolve(false)); }); }; const onNotification = data => { if (data.payload === PING_MESSAGE) return; if (data.payload === SELF_CHECK_MESSAGE) { return this.selfCheckCallback && this.selfCheckCallback(); } let message = data.payload; let isMessageOk = true; if (this.parseJson) { try { message = JSON.parse(data.payload); } catch (e) { isMessageOk = false; this.logger(data.payload); this.logger(e); this.logger(data); } } if (isMessageOk && this.onDatabaseNotification) { this.onDatabaseNotification(message); } else { this.logger(message); } }; const setListeners = client => { client.on('notification', onNotification); return this.connection.none('LISTEN $1~', this.channel).catch(error => { this.logger(error); // unlikely to happen }); }; const removeListeners = client => { client.removeListener('notification', onNotification); }; const onConnectionLost = (err, e) => { this.logger('Connectivity Problem:', err); this.connection = null; // prevent use of the connection removeListeners(e.client); reconnect(this.retryInterval, this.maxRetryCount) .then(() => { this.logger('Successfully Reconnected'); }) .catch(() => { this.logger('Connection Lost Permanently'); process.exit(); // exiting the process }); }; const reconnect = (delay, maxAttempts) => { delay = delay > 0 ? parseInt(delay) : 0; maxAttempts = maxAttempts > 0 ? parseInt(maxAttempts) : 1; return new Promise((resolve, reject) => { setTimeout(() => { this.db .connect({direct: true, onLost: onConnectionLost}) .then(obj => { // global connection is now available this.connection = obj; resolve(obj); return setListeners(obj.client); }) .catch(error => { this.logger('Error Connecting:', error); if (--maxAttempts) { reconnect(delay, maxAttempts) .then(resolve) .catch(reject); } else { reject(error); } }); }, delay); }); }; const sendNotifications = () => { //Initiate a "notify" from the database to check that we are connected setInterval(() => { if (this.connection) { this.connection.none('NOTIFY $1~, $2', [this.channel, PING_MESSAGE]).catch(error => { this.logger('Failed to Notify:', error); // unlikely to happen }); } }, PING_INTERVAL); }; const init = () => { return reconnect() // same as reconnect(0, 1) .then(obj => { this.logger('Successful Initial database Connection'); sendNotifications(); }) .catch(error => { this.logger('Failed Initial database Connection:', error); }); }; init(); };