tibbarkcaj
Version:
Easy RabbitMQ for node
470 lines (352 loc) • 12 kB
JavaScript
;
const Extend = require('lodash.assignin');
const { EventEmitter, once } = require('events');
const { v4: Uuid } = require('uuid');
const { Writable } = require('stream');
const Queue = require('./queue');
const DEFAULT_EXCHANGES = {
'direct': 'amq.direct',
'fanout': 'amq.fanout',
'topic': 'amq.topic'
};
const DEFAULT_EXCHANGE_OPTIONS = {
durable: true,
noReply: true,
internal: false,
autoDelete: false,
alternateExchange: undefined
};
const DEFAULT_PUBLISH_OPTIONS = {
contentType: 'application/json',
mandatory: false,
persistent: false,
expiration: undefined,
userId: undefined,
CC: undefined,
BCC: undefined
};
const DEFAULT_RPC_CLIENT_OPTIONS = {
timeout: 3000,
prefetch: 1,
durable: false,
autoDelete: true
};
const isDefault = (name, type) => {
return DEFAULT_EXCHANGES[type] === name;
};
const isNameless = (name) => {
return name === '';
};
const exchange = (name, type, exchangeOptions) => {
if (!type) {
throw new Error('missing exchange type');
}
if (!isNameless(name)) {
name = name || DEFAULT_EXCHANGES[type];
if (!name) {
throw new Error('missing exchange name');
}
}
let ready = false;
let blocked = false;
let connecting = false;
let channel;
let connection;
let publishing = 0;
let replyQueueConfigured = false;
const options = Extend({}, DEFAULT_EXCHANGE_OPTIONS, exchangeOptions);
const replyQueue = options.noReply ? null : Queue({ exclusive: true });
const activeQueues = [];
const pendingReplies = {};
const rpcClient = (key, msg, rpcOptions, cb) => {
if (!key) {
throw new Error('missing rpc method');
}
if (!replyQueue) {
throw new Error('replyQueue not set - ensure { noReply: false } is passed to exchange options');
}
if (!cb && typeof rpcOptions === 'const') {
cb = rpcOptions;
}
if (!rpcOptions || typeof rpcOptions !== 'object') {
rpcOptions = DEFAULT_RPC_CLIENT_OPTIONS;
}
const opts = Extend({}, {
key,
rpcCallback: cb
}, rpcOptions);
publish(msg, opts);
};
const rpcServer = (key, handler) => {
if (!replyQueue) {
throw new Error('replyQueue not set - ensure { noReply: false } is passed to exchange options');
}
const rpcQueue = createQueue({
key,
name: key,
prefetch: 1,
durable: false,
autoDelete: true
});
rpcQueue.consume(handler);
};
const doWhenReady = (readyFlag, fn) => {
if (readyFlag) {
fn();
}
else {
emitter.once('ready', fn);
}
};
const connect = (con) => {
connecting = true;
connection = con;
connection.createChannel(onChannel);
connection.on('blocked', onBlocked);
connection.on('unblocked', onUnBlocked);
if (replyQueue) {
replyQueue.once('close', bail.bind(this));
if (!replyQueueConfigured && ready) { // This should only happen once
replyQueueConfigured = true;
replyQueue.consume(onReply, { noAck: true });
}
}
doWhenReady(ready, () => {
activeQueues.forEach((queue) => queue.connect(connection));
connecting = false;
});
return emitter;
};
const createQueue = (queueOptions) => {
// return a promise when all keys are bound
const bindKeys = (keys) => {
// returns a promise when a key is bound
const bindKey = (key) => {
return new Promise(((resolve, reject) => {
channel.bindQueue(newQueue.amqLabel, emitter.name, key, {}, (err, ok) => {
if (err) {
return reject(err);
}
return resolve(ok);
});
}));
};
return Promise.all(keys.map(bindKey));
};
const newQueue = Queue(queueOptions);
newQueue.once('close', bail.bind(this));
newQueue.once('ready', () => {
// the default exchange has implicit bindings to all queues
if (!isNameless(emitter.name)) {
const keys = queueOptions.keys || [queueOptions.key];
bindKeys(keys)
.then((res) => {
newQueue.emit('bound');
})
.catch(bail);
}
});
activeQueues.push(newQueue);
if (connection && ready && !connecting) {
newQueue.connect(connection);
}
return newQueue;
};
const getWritableStream = () => {
return new Writable({
objectMode: true,
write({ key, data, headers }, encoding, cb) {
const ok = sendMessage(data, { key, headers });
if (ok) {
process.nextTick(cb);
}
else {
channel.once('drain', cb);
}
}
});
};
const publishSafe = async (message, publishOptions) => {
publishing++;
publishOptions = publishOptions || {};
const sendMessageRef = publishOptions.rpcCallback ? sendRpcMessage : sendMessage;
if (!ready) {
await once(emitter, 'ready');
}
if (blocked) {
await once(emitter, 'unblocked');
}
return sendMessageRef(message, publishOptions);
};
const publish = (message, publishOptions) => {
publishing++;
publishOptions = publishOptions || {};
const sendMessageRef = publishOptions.rpcCallback ? sendRpcMessage : sendMessage;
doWhenReady(ready, () => sendMessageRef(message, publishOptions));
return emitter;
};
const sendMessage = (message, publishOptions) => {
// TODO: better blacklisting/whitelisting of properties
const opts = Extend({}, DEFAULT_PUBLISH_OPTIONS, publishOptions);
const msg = encodeMessage(message, opts.contentType);
if (opts.reply) {
if (!replyQueue) {
throw new Error('reply queue not found');
}
opts.replyTo = replyQueue.amqLabel;
opts.correlationId = Uuid();
pendingReplies[opts.correlationId] = opts.reply;
delete opts.reply;
}
const drained = channel.publish(emitter.name, opts.key, Buffer.from(msg), opts);
if (drained) {
onDrain();
}
return drained;
};
const sendRpcMessage = (message, publishOptions) => {
const opts = Extend({}, DEFAULT_PUBLISH_OPTIONS, publishOptions);
const msg = encodeMessage(message, opts.contentType);
let replied = false;
const correlationId = Uuid();
const rpcCallback = opts.rpcCallback;
const onReply = (reply) => {
clearTimeout(timeout);
channel.removeListener('return', onNotFound);
if (!replied) {
replied = true;
rpcCallback(reply);
}
};
const onNotFound = (notFound) => {
clearTimeout(timeout);
clearPendingReply(correlationId);
channel.removeListener('return', onNotFound);
if (!replied) {
replied = true;
rpcCallback(new Error('Not Found'));
}
};
const timeout = setTimeout(() => {
clearPendingReply(correlationId);
channel.removeListener('return', onNotFound);
if (!replied) {
replied = true;
rpcCallback(new Error('Timeout'));
}
}, publishOptions.timeout || DEFAULT_RPC_CLIENT_OPTIONS.timeout);
opts.replyTo = replyQueue.amqLabel;
opts.correlationId = correlationId;
opts.mandatory = true;
pendingReplies[opts.correlationId] = onReply;
channel.once('return', onNotFound);
const drained = channel.publish(emitter.name, opts.key, Buffer.from(msg), opts);
if (drained) {
onDrain();
}
return drained;
};
const encodeMessage = (message, contentType) => {
if (contentType === 'application/json') {
return JSON.stringify(message);
}
return message;
};
const onReply = (data, ack, nack, msg) => {
const replyCallback = pendingReplies[msg.properties.correlationId];
if (replyCallback) {
replyCallback(data);
}
clearPendingReply(msg.properties.correlationId);
};
const clearPendingReply = (correlationId) => {
delete pendingReplies[correlationId];
};
const bail = (err) => {
// TODO: close all queue channels?
connection = undefined;
channel = undefined;
emitter.emit('close', err);
};
const onBlocked = (cause) => {
blocked = true;
emitter.emit('blocked', cause);
};
const onUnBlocked = () => {
blocked = false;
emitter.emit('unblocked');
};
const onDrain = () => {
setImmediate(() => {
publishing--;
if (publishing === 0) {
emitter.emit('drain');
}
});
};
const onChannel = (err, chan) => {
if (err) {
return bail(err);
}
channel = chan;
channel.once('close', bail.bind(this, new Error('channel closed')));
channel.on('drain', onDrain);
emitter.emit('connected');
if (isDefault(emitter.name, DEFAULT_EXCHANGES[emitter.type]) || isNameless(emitter.name)) {
onExchange(undefined, {
exchange: emitter.name
});
}
else {
channel.assertExchange(emitter.name, emitter.type, emitter.options, onExchange);
}
};
const onExchange = (err, info) => {
if (err) {
return bail(err);
}
if (!replyQueue) {
ready = true;
emitter.emit('ready');
return;
}
replyQueue.connect(connection);
replyQueue.once('ready', () => {
if (!replyQueueConfigured) { // This should only happen once
replyQueueConfigured = true;
replyQueue.consume(onReply, { noAck: true });
}
ready = true;
emitter.emit('ready');
});
};
const getInternals = () => {
const internals = {
// amqplib instances
connection,
channel,
// internal state
ready,
blocked,
connecting,
publishing,
replyQueueConfigured,
}
return internals
}
const emitter = Extend(new EventEmitter(), {
name,
type,
options,
queue: createQueue,
connect,
publish,
publishSafe,
getWritableStream,
rpcClient,
rpcServer,
getInternals,
});
return emitter;
};
module.exports = exchange;