servicebus-register-handlers
Version:
module for registering all servicebus handlers in a folder, according to convention
267 lines (188 loc) • 8.51 kB
JavaScript
var amqpMatch = require('amqp-match');
var async = require('async');
var debug = require('debug')('register-handlers');
var objectifyFolder = require('objectify-folder');
var trace = require('debug')('register-handlers:trace');
var util = require('util');
var warn = require('debug')('register-handlers:warn');
function QueuePipeline (options) {
this.handlers = [];
this.queueName = options.queueName;
this.size = 0;
}
QueuePipeline.prototype.push = function (handler) {
this.size++;
return this.handlers.push(handler);
};
function RoutingKeyPipeline (options) {
this.handlers = [];
this.routingKey = options.routingKey;
this.size = 0;
}
RoutingKeyPipeline.prototype.push = function (handler) {
this.size++;
return this.handlers.push(handler);
};
function Handler (options) {
if (options.where && typeof options.where !== 'function') throw new Error('module.exports.where must be of type function and return a boolean statement');
if (options.listen && ! (options.queueName || options.command)) throw new Error('module.exports.listen must be accompanied by a module.exports.queueName specification.')
if (options.subscribe && ! (options.routingKey || options.event)) throw new Error('module.exports.subscribe must be accompanied by a module.exports.routingKey specification.')
if (options.listen && options.subscribe) throw new Error('module.exports.listen and module.exports.subscribe cannot both be specified on a handler.')
this.ack = options.ack ? options.ack : (options.command || options.event) ? true : undefined;
this.listen = options.listen;
this.queueName = options.queueName || options.command;
this.routingKey = options.routingKey || options.event;
this.subscribe = options.subscribe;
this.type = options.type;
this.where = options.where;
}
function addHandler (pipelines, handler) {
debug('adding Handler - pipelines', pipelines)
debug('adding Handler - handler', handler)
if (handlerIsOrSharesListenQueue(handler)) {
if ( ! pipelines[handler.queueName]) pipelines[handler.queueName] = new QueuePipeline({ queueName: handler.queueName });
pipelines[handler.queueName].push(handler);
} else {
if ( ! pipelines[handler.routingKey]) pipelines[handler.routingKey] = new RoutingKeyPipeline({ routingKey: handler.routingKey });
pipelines[handler.routingKey].push(handler);
}
}
function handlerIsOrSharesListenQueue (handler) {
return handler.queueName !== undefined;
};
function prepareOptions (options) {
options = options || {};
options.pipelines = {};
if ( ! options.handlers && ! options.path) throw new Error('register-handlers requires a folder path or object of required modules');
if (options.path) {
var i = 0;
var handlers = objectifyFolder({
fn: function (mod, result) {
if ( ! (mod.queueName || mod.routingKey || mod.command || mod.event ) || ! (mod.listen || mod.subscribe)) return;
addHandler(options.pipelines, new Handler(mod));
},
path: options.path
});
} else if (options.handlers) {
options.handlers.forEach(function (handler) {
addHandler(options.pipelines, handler);
});
}
return options;
}
function registerPipeline (options, pipeline) {
debug('registering pipeline', pipeline)
var bus = options.bus;
var firstHandler = pipeline.handlers[0];
var isAck = firstHandler.ack;
var isListen = firstHandler.listen !== undefined;
// var hasQueueNameSpecified = firstHandler.queueName !== undefined;
var method = (isListen) ? 'listen' : 'subscribe';
// var queueName = hasQueueNameSpecified ? firstHandler.queueName :
// options.queuePrefix !== undefined ? util.format('%s-', queueName) : firstHandler.routingKey;
// var queueName = firstHandler.queueName;
var queueName = ! isListen ?
(firstHandler.queueName) ? firstHandler.queueName :
(options.queuePrefix !== undefined ? util.format(options.queuePrefix + '-%s', firstHandler.routingKey) : firstHandler.routingKey) :
firstHandler.routingKey || firstHandler.queueName;
debug('registering pipeline: queuename', queueName)
debug('registering pipeline: method', method)
function handleError (msg, err) {
debug('error handling message with cid ', msg.cid);
debug(err.stack || err.message || err);
if (firstHandler.ack) msg.handle.reject(function () {
throw err;
});
}
function handleIncomingMessage (pipeline, msg, message) {
var context = {
queueName: message.fields.queueName,
routingKey: message.fields.routingKey,
correlationId: message.properties.correlationId,
bus: bus
};
var handlers = pipeline.handlers.filter(function (handler) {
return (handler.routingKey === undefined || (handler.routingKey !== undefined && amqpMatch(message.fields.routingKey, handler.routingKey))) &&
(handler.type === undefined || handler.type === msg.type) &&
(handler.where === undefined || handler.where && handler.where(msg));
});
if (handlers.length === 0) {
warn('no handler registered to handle %j', msg);
if (isAck) msg.handle.ack();
return;
}
trace('handling message: %j', msg);
if (process.domain) process.domain.once('error', (options.handleError || handleError).bind(context, msg));
async.map(handlers, function (handler, cb) {
try {
handler[method].call(context, msg, cb);
} catch (err) {
if (err) return (options.handleError || handleError).call(context, msg, err);
trace('handled message with error: %j', msg);
if (isAck) return msg.handle.ack(cb);
else cb();
}
}, function (err) {
if (err) return (options.handleError || handleError).call(context, msg, err);
if (isAck) msg.handle.ack();
if (options.onHandlerCompleted) options.onHandlerCompleted.call(context, msg, message);
trace('handled message: %j', msg);
});
}
var obj = bus[method].call(bus, queueName,
{ ack: isAck, routingKey: firstHandler.routingKey },
handleIncomingMessage.bind(bus, pipeline));
if (process.env.NODE_ENV === 'test') {
pipeline.handleIncomingMessage = handleIncomingMessage.bind(bus, pipeline)
}
if (pipeline.size === 1 || pipeline instanceof RoutingKeyPipeline) return;
pipeline.handlers.filter(function (h) { return h !== firstHandler; }).forEach(function (handler) {
if (handler.ack !== isAck) throw new Error('module.exports.ack for %s handlers do not match', firstHandler.queueName || firstHandler.routingKey);
var queueType = method === 'subscribe' ? 'pubsubqueues' : 'queues';
function bindHandler () {
if (pipeline.handlers.some(function (h) { return handler.routingKey !== h.routingKey; })) {
if (bus[queueType][queueName].listening) {
bus[queueType][queueName].listenChannel.bindQueue(queueName, bus.exchangeName, handler.routingKey);
} else {
bus[queueType][queueName].on('listening', function () {
bus[queueType][queueName].listenChannel.bindQueue(queueName, bus.exchangeName, handler.routingKey);
});
}
}
}
if (bus.initialized) bindHandler();
else bus.on('ready', bindHandler);
});
}
module.exports = function (options) {
if ( ! options.bus) throw new Error('register-handlers requires in initialized bus variable');
if (! options.modules) {
prepareOptions(options);
Object.keys(options.pipelines).forEach(function (key) {
var pipeline = options.pipelines[key];
registerPipeline(options, pipeline);
});
return options;
} else {
return new Promise((resolve, reject) => {
objectifyFolder = require('objectify-folder/modules')
options.pipelines = {}
objectifyFolder({
fn: function (mod, result) {
debug('imported module', mod)
if ( ! (mod.queueName || mod.routingKey || mod.command || mod.event ) || ! (mod.listen || mod.subscribe)) return;
addHandler(options.pipelines, new Handler(mod));
},
path: options.path
}).then((modules) => {
debug('objectified folder - options', options)
Object.keys(options.pipelines).forEach(function (key) {
var pipeline = options.pipelines[key];
registerPipeline(options, pipeline);
});
resolve(options)
})
})
}
};
module.exports.Handler = Handler;