tibbarkcaj
Version:
Easy RabbitMQ for node
221 lines (169 loc) • 6.24 kB
JavaScript
;
const Amqp = require('amqplib/callback_api');
const AmqpConnection = require('amqplib/lib/connection');
const Extend = require('lodash.assignin');
const EventEmitter = require('events').EventEmitter;
const Exchange = require('./exchange');
const jackrabbit = (url, logger, options = {}) => {
if (!url) {
throw new Error('url required for jackrabbit connection');
}
options.reconnectionTimeout = options.reconnectionTimeout || +process.env.RABBIT_RECONNECTION_TIMEOUT || 2000;
options.maxRetries = options.maxRetries || +process.env.RABBIT_RECONNECTION_RETRIES || 20;
if (process.env.RABBIT_RECONNECTION_EXACT_TIMEOUT !== 'true') {
options.reconnectionTimeout = Math.floor(options.reconnectionTimeout * (1 + Math.random() * 0.1));
}
// state
let connection;
let connectionAttempts = 0;
const exchanges = [];
const pendingExchangesForConnection = [];
// public
const getInternals = () => {
return {
amqp: Amqp,
connection
};
};
const isConnectionReady = () => {
return Boolean(connection?.connection?.stream?.writable);
};
const close = (callback) => {
if (!connection) {
if (callback) {
callback();
}
return;
}
try {
// I don't think amqplib should be throwing here, as this is an async const
// TODO: figure out how to test whether or not amqplib will throw
// (eg, how do they determine if closing is an illegal operation?)
connection.close((err) => {
if (callback) {
callback(err);
}
rabbit.emit('close');
});
}
catch (e) {
if (callback) {
callback(e);
}
}
};
const createDefaultExchange = () => {
return createExchange()('direct', '', { noReply: false });
};
const createExchange = () => {
return (type, name, exchangeOptions) => {
const newExchange = Exchange(name, type, exchangeOptions);
exchanges.push(newExchange);
if (connection) {
connection.setMaxListeners(exchanges.length + 10);
newExchange.connect(connection);
}
else {
pendingExchangesForConnection.push(newExchange);
}
return newExchange;
};
};
// private
const bail = (err, newConnectionAttempt = false) => {
// TODO close any connections or channels that remain open
connection = undefined;
if (!err) {
return;
}
if (newConnectionAttempt && tryReconnect(err)) {
return;
}
if (!newConnectionAttempt && (err.onConnectionErrorEvent || !AmqpConnection.isFatalError(err)) && tryReconnect(err)) {
return;
}
rabbit.emit('error', err);
doLog('fatal', 'Rabbit connection error!');
process.exit(1);
};
const doLog = (level, message) => {
if (typeof logger?.[level] === 'function') {
logger[level](message);
}
else if (typeof logger?.log === 'function') {
logger.log(level, message);
}
else {
rabbit.emit(level, message);
}
};
const tryReconnect = (err) => {
if (connectionAttempts >= options.maxRetries) {
err.meta = 'Error connecting to RabbitMQ';
return false;
}
const doReconnect = () => {
++connectionAttempts;
rabbit.emit('reconnecting');
doLog('info', `Reconnecting to RabbitMQ (${connectionAttempts}/${options.maxRetries})...`);
Amqp.connect(url, onConnection);
};
if (connectionAttempts === 0) {
doLog('warn', `Lost connection to RabbitMQ! Reconnecting in ${options.reconnectionTimeout}ms...`);
doReconnect();
}
else {
setTimeout(doReconnect, options.reconnectionTimeout);
}
return true;
};
const onConnection = (err, conn) => {
if (err) {
return bail(err, true);
}
connection = conn;
connection.setMaxListeners(exchanges.length + 10);
connection.once('close', bail.bind(this));
connection.on('error', (err) => {
err.onConnectionErrorEvent = true;
});
connection.on('blocked', (cause) => rabbit.emit('blocked', cause));
connection.on('unblocked', () => rabbit.emit('unblocked'));
const notifyReady = () => {
rabbit.emit(connectionAttempts > 0 ? 'reconnected' : 'connected');
if (connectionAttempts > 0) {
doLog('info', 'Reconnected to RabbitMQ');
connectionAttempts = 0;
}
};
const pendingExchanges = connectionAttempts > 0 ? exchanges : pendingExchangesForConnection;
if (pendingExchanges.length === 0) {
notifyReady();
}
else {
let readyCount = 0;
pendingExchanges.forEach((exchange) => {
exchange.connect(connection);
exchange.once('ready', () => {
++readyCount;
if (readyCount === pendingExchanges.length) {
notifyReady();
}
});
});
}
};
const rabbit = Extend(new EventEmitter(), {
default: createDefaultExchange,
direct: createExchange().bind(null, 'direct'),
fanout: createExchange().bind(null, 'fanout'),
topic: createExchange().bind(null, 'topic'),
exchange: createExchange(),
close,
getInternals,
isConnectionReady
});
Amqp.connect(url, onConnection);
return rabbit;
};
module.exports = jackrabbit;