UNPKG

node-busmq

Version:

A high performance, highly-available and scalable, message bus and queueing system for node.js backed by Redis

349 lines (307 loc) 9.85 kB
var events = require('events'); var util = require('util'); var Queue = require('./queue'); function _noop() {}; /** * Creates a new bi-directional message channel between two endpoints utilizing message queues. * @param bus * @param name * @param local * @param remote * @constructor */ function Channel(bus, name, local, remote) { events.EventEmitter.call(this); this.bus = bus; this.name = name; this.type = 'channel'; this.id = 'bus:channel:' + name; this.logger = bus.logger.withTag(name); this.local = local || 'local'; this.remote = remote || 'remote'; this.handlers = { '1': this._onRemoteConnect, '2': this._onMessage, '3': this._onRemoteDisconnect, '4': this._onEnd } } util.inherits(Channel, events.EventEmitter); /** * Connect to the channel, using the 'local' role to consume messages and 'remote' role to send messages. * connect - emitted when this endpoint is connected to the channel * remote:connect - emitted when the remote endpoint connects to the channel * disconnect - emitted when this endpoint is disconnected from the channel * remote:disconnect - emitted when the remote endpoint is disconnected from the channel * message - emitted when a message is received from the peer * end - emitted when the peer disconnects from the channel * @param options message consumption options (same as Queue#consume) */ Channel.prototype.connect = function(options) { if (this.isAttached()) { this.emit('error', 'cannot connect: channel is already bound'); return; } this.options = util._extend({}, options); this.qConsumer = new Queue(this.bus, 'channel:' + this.local + ':' + this.name); this.qProducer = new Queue(this.bus, 'channel:' + this.remote + ':' + this.name); this._attachQueues(); }; Channel.prototype.attach = Channel.prototype.connect; /** * Connect to the channel as a "listener", using the 'local' role to send messages and 'remote' role to consume * messages. This is just a syntactic-sugar for a connect with a reverse semantic of the local/remote roles. Events: * connect - emitted when this endpoint is connected to the channel remote:connect - emitted when the remote endpoint * connects to the channel disconnect - emitted when this endpoint is disconnected from the channel remote:disconnect - * emitted when the remote endpoint is disconnected from the channel message - emitted when a message is received from * the peer end - emitted when the peer disconnects from the channel * @param options message consumption options (same as Queue#consume) */ Channel.prototype.listen = function(options) { var remote = this.remote; this.remote = this.local; this.local = remote; this.connect(options); }; /** * Send a message to the other endpoint * @param message the message to send * @param cb invoked after the message has actually been sent */ Channel.prototype.send = function(message, cb) { if (!this.isAttached()) { return; } this.qProducer.push(':2:'+message, cb); }; /** * Send a message to the specified endpoint without connecting to the channel * @param endpoint * @param msg * @param cb * @private */ Channel.prototype.sendTo = function(endpoint, msg, cb) { var _this = this; var q = new Queue(this.bus, 'channel:' + endpoint + ':' + this.name); q.on('error', function(err) { _this.logger.isDebug() && _this.logger.debug('error on channel sendTo ' + q.name + ': ' + err); q.detach(); cb && cb(err); cb = null; }); q.on('attached', function() { try { q.push(':2:' + msg, cb); } finally { q.detach(); } }); q.attach(); }; /** * Disconnect this endpoint from the channel without sending the 'end' event to the remote endpoint. * The channel will remain open. */ Channel.prototype.disconnect = function() { if (!this.isAttached()) { return; } this.qProducer.push(':3:'); this._detachQueues(); }; Channel.prototype.detach = Channel.prototype.disconnect; /** * Ends the channel and causes both endpoints to disconnect. * No more messages can be sent or will be received. */ Channel.prototype.end = function() { if (!this.isAttached()) { return; } this.qProducer.push(':4:'); this._detachQueues(); }; /** * Returns whether this channel is bound to the message queues */ Channel.prototype.isAttached = function() { return this.qProducer && this.qConsumer; }; /** * Ack the specified message id. applicable only if consuming in reliable mode. * @param id the message id to ack * @param cb invoked when the ack is complete */ Channel.prototype.ack = function(id, cb) { if (!this.isAttached()) { return; } this.qConsumer.ack(id, cb); }; /** * Attach to the bus queues * @private */ Channel.prototype._attachQueues = function() { var _this = this; var attachedCount = 0; var detachedCount = 2; var _attached = function() { if (++attachedCount === 2) { _this.emit('connect'); } }; var _detached = function() { if (--detachedCount === 0) { _this.emit('disconnect'); _attached = undefined; _detached = undefined; } }; var onConsumerError = function(err) { if (_this.qConsumer) { _this.emit('error', err); } }; var onCosumerAttached = function() { if (_this.qConsumer) { _attached(); _this.qConsumer.consume(_this.options); } }; var onCosumerMessage = function(msg, id) { if (_this.qConsumer) { _this._handleMessage(msg, id); } }; var onConsumerDetached = function() { // register an empty error listener so errors don't throw exceptions _this.qConsumer.on('error', _noop); _this.qConsumer.removeListener('error', onConsumerError); onConsumerError = undefined; _this.qConsumer.removeListener('attached', onCosumerAttached); onCosumerAttached = undefined; _this.qConsumer.removeListener('message', onCosumerMessage); onCosumerMessage = undefined; _this.qConsumer.removeListener('detached', onConsumerDetached); onConsumerDetached = undefined; _this.qConsumer = null; _detached(); }; this.qConsumer.on('error', onConsumerError); this.qConsumer.on('message', onCosumerMessage); this.qConsumer.on('attached', onCosumerAttached); this.qConsumer.on('detached', onConsumerDetached); this.qConsumer.attach(this.options); var onProducerError = function(err) { if (_this.qProducer) { _this.emit('error', err); } }; var onProducerAttached = function() { if (_this.qProducer) { _attached(); _this.qProducer.push(':1:'); } }; var onProducerDetached = function() { // register an empty error listener so errors don't throw exceptions _this.qProducer.on('error', _noop); _this.qProducer.removeListener('error', onProducerError); onProducerError = undefined; _this.qProducer.removeListener('attached', onProducerAttached); onProducerAttached = undefined; _this.qProducer.removeListener('detached', onProducerDetached); onProducerDetached = undefined; _this.qProducer = null; _detached(); }; this.qProducer.on('error', onProducerError); this.qProducer.on('attached', onProducerAttached); this.qProducer.on('detached', onProducerDetached); this.qProducer.attach(this.options); }; /** * Handle a message * @param msg the received message * @param id id of the received message * @private */ Channel.prototype._handleMessage = function(msg, id) { if (msg.length >= 3 && msg.charAt(0) === ':' && msg.charAt(2) === ':') { var type = msg.charAt(1); var message = msg.substring(3); this.handlers[type].call(this, message, id); } else { this.emit('error', 'received unrecognized message type'); } }; /** * Handle a remote:connect event * @private */ Channel.prototype._onRemoteConnect = function(message, id) { id && this.ack(id); if (!this._remoteConnected) { this._remoteConnected = true; this.emit('remote:connect'); } }; /** * Handle a received message * @private */ Channel.prototype._onMessage = function(msg, id) { this.emit('message', msg, id); }; /** * Handle a remote:disconnect event * @private */ Channel.prototype._onRemoteDisconnect = function(message, id) { // puh a new 'connect' message so that a new connecting remote // will know we are connected regardless of any other messages we sent this.qProducer.push(':1:'); this.ack(id); if (this._remoteConnected) { this._remoteConnected = false; this.emit('remote:disconnect'); } }; /** * Handle an end event * @private */ Channel.prototype._onEnd = function(message, id) { this.ack(id); this._detachQueues(); this.emit('end'); }; /** * Detach from the bus queues * @private */ Channel.prototype._detachQueues = function() { if (this.qConsumer) { this.qConsumer.detach(); } if (this.qProducer) { this.qProducer.detach(); } }; /** * Convert all eligible methods into promise based methods instead of callback based methods */ Channel.prototype.promisify = function() { return this.bus.promisify(this, ["send", "sendTo", "ack"]); }; /** * Tells the federation object which methods save state that need to be restored upon * reconnecting over a dropped websocket connection * @private */ Channel.prototype._federationState = function() { return [{save: 'connect', unsave: 'disconnect'}, {save: 'connect', unsave: 'end'}, {save: 'attach', unsave: 'detach'}]; }; exports = module.exports = Channel;