UNPKG

node-busmq

Version:

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

370 lines (320 loc) 9.08 kB
var EventEmitter = require('events').EventEmitter; var util = require('util'); var crypto = require('crypto'); var WebSocket = require('ws'); /** * A no-op function used for catching and ignoring async errors in ws.send * @private */ function _noop() {} /** * A channel on a mulitplexed websocket * @param id the id of the channel * @param mux the parent websocket multiplexer * @constructor */ function WSMuxChannel(id, mux) { EventEmitter.call(this); this.id = id; this.mux = mux; this.closed = false; this.url = this.mux.url; // websocket-stream relies on this method this.__defineGetter__('readyState', function() { return this.mux._readyState(); }); // hooks for websocket-stream var _this = this; this.on('open', function() { _this.onopen && _this.onopen.apply(_this, arguments); }); this.on('close', function() { _this.onclose && _this.onclose.apply(_this, arguments); }); this.on('error', function() { _this.onerror && _this.onerror.apply(_this, arguments); }); this.on('message', function() { arguments[0] = {data: arguments[0]} _this.onmessage && _this.onmessage.apply(_this, arguments); }); } util.inherits(WSMuxChannel, EventEmitter); /** * Send a message on the channel * @param message * @param cb */ WSMuxChannel.prototype.send = function(message, cb) { if (this.closed) { this.emit('error', 'cannot send on closed channel'); return; } this.mux._sendMessage(this.id, message, cb); }; /** * Close this channel */ WSMuxChannel.prototype.close = function() { this.mux._closeChannel(this.id, true, 'channel close requested'); this.closed = true; }; /** * Websocket multiplexer * @param urlOrWs the url to open the webscoket to multiplex * @param options * @constructor */ function WSMux(urlOrWs, options) { EventEmitter.call(this); this.options = util._extend({mask: true}, options); this.closed = false; this.channels = {}; this.pending = []; this._openWebsocket(urlOrWs); } util.inherits(WSMux, EventEmitter); /** * attach a websocket to this multiplexer * @private */ WSMux.prototype._openWebsocket = function(urlOrWs) { var _this = this; var heartbeatTimer; var clearHeartbeat = function() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } }; var onOpen = function() { heartbeatTimer = setInterval(function() { ws.ping('hb', {}, true); }, 10*1000); // send any pending messages _this.pending.forEach(function(msg) { _this._wsSend(msg, null); }); _this.pending = []; _this.emit('open'); }; var onClose = function(code) { _this.emit('close', code); replace(); }; var onError = function(error) { _this.emit('error', error); replace(_this.options.replaceDelay); }; var onUnexpectedResponse = function(req, res) { // 401 means wrong secret key if (res.statusCode === 401) { _this.emit('fatal', 'unauthorized'); // do not replace the websocket shutdown(); } else { _this.emit('error', 'websocket received unexpected response: ' + res.statusCode); // try to open the websocket again replace(_this.options.replaceDelay); } }; var onMessage = function(message) { _this._onMessage(message); }; var replace = function(delay) { shutdown(); if (_this.closed || !reopen) { return; } _this.emit('reopen'); // open a new websocket. // we either do it immediately or delay it by the amount specified setTimeout(function() { _this._openWebsocket(url); }, delay || 0); }; var shutdown = function() { _this._closeAllChannels(false, 'websocket closed'); clearHeartbeat(); clearHeartbeat = undefined; ws.removeListener('open', onOpen); onOpen = undefined; ws.removeListener('close', onClose); onClose = undefined; ws.removeListener('error', onError); onError = undefined; ws.removeListener('unexpected-response', onUnexpectedResponse); onUnexpectedResponse = undefined; ws.removeListener('message', onMessage); onMessage = undefined; ws.removeListener('replace', replace); replace = undefined; ws.removeListener('shutdown', shutdown); shutdown = undefined; delete _this.ws; ws.close(); ws = undefined; }; var url, ws, reopen; if (typeof urlOrWs === 'string') { reopen = true; url = urlOrWs; try { ws = new WebSocket(url + '?secret=' + this.options.secret); } catch (e) { onError('error creating web socket: ' + e.message); return; } } else { reopen = false; ws = urlOrWs; url = ws.url; } this.url = url; ws.on('open', onOpen); ws.on('close', onClose); ws.on('error', onError); ws.on('unexpected-response', onUnexpectedResponse); ws.on('message', onMessage); ws.on('replace', replace); ws.on('shutdown', shutdown); this.ws = ws; }; /** * Get the websocket ready state * @private */ WSMux.prototype._readyState = function() { return this.ws.readyState; }; /** * Create a new channel over the multiplexed websocket * @param id * @returns {WSMuxChannel} */ WSMux.prototype.channel = function(id) { return this._createChannel(id); }; /** * Create a new channel * @param id * @returns {WSMuxChannel} * @private */ WSMux.prototype._createChannel = function(id) { id = id || crypto.randomBytes(8).toString('hex'); var channel = new WSMuxChannel(id, this); this.channels[id] = channel; return channel; }; /** * Send a message on the channel * @param id id of the channel * @param message the message to send * @private */ WSMux.prototype._sendMessage = function(id, message, cb) { this._wsSend({id: id, msg: message}, cb); }; /** * Close a channel * @param id the id of the channel to close * @param send whether to send the other side that we want to close this channel * @param message close reason * @private */ WSMux.prototype._closeChannel = function(id, send, message) { if (send) { this._wsSend({id: id, close: true}, null); } var _this = this; process.nextTick(function() { if (_this.channels[id]) { var channel = _this.channels[id]; delete _this.channels[id]; channel.emit('close', message); } }); }; /** * Closes all channels * @param send whether to send the other side that this channel is closed * @param message close reason * @private */ WSMux.prototype._closeAllChannels = function(send, message) { var _this = this; Object.keys(this.channels).forEach(function(id) { _this._closeChannel(id, send, message); }); }; /** * Send a message now or later when the websocket is open * @param msg * @param cb * @private */ WSMux.prototype._wsSend = function(msg, cb) { if (!this.channels[msg.id]) { return; } // create a packet that will be sent over the wire. // the packet is a buffer containing: <16 bytes channel id><1 byte packet type><message payload> if (this.ws && this.ws.readyState === 1) { var data; var msgId = Buffer.from(msg.id, 'ascii'); if (msg.msg) { data = Buffer.concat([msgId, Buffer.from('1', 'ascii'), Buffer.from(msg.msg, 'utf8')]); } else if (msg.close) { data = Buffer.concat([msgId, Buffer.from('2', 'ascii')]); } this.ws.send(data, {binary: true, mask: this.options.mask}, cb || _noop); } else { this.pending.push(msg); } }; /** * Handle a message received on the weboscket and pass it on to the correct channel * @param message * @private */ WSMux.prototype._onMessage = function(message) { // first 16 chars are the channel id, next char is the type of message var packet = { id: message.slice(0, 16).toString(), type: message.slice(16, 17).toString() }; if (packet.type === '1') { packet.msg = message.slice(17); } else if (packet.type === '2') { packet.close = true; } var channel = this.channels[packet.id]; // close the channel if (packet.close) { if (channel) { // because we got the close from the other end, no need to send it over this._closeChannel(packet.id, false, 'remote end closed the channel'); } return; } // if we don't have a channel, create one now if (!channel) { channel = this._createChannel(packet.id); this.emit('channel', channel); } var msg = packet.msg; channel.emit('message', msg); }; /** * Close this websocket multiplexer */ WSMux.prototype.close = function () { this.closed = true; this._closeAllChannels(true, 'multiplexer closed'); var _this = this; process.nextTick(function() { _this.emit('shutdown'); }); }; exports = module.exports = WSMux;