UNPKG

node-busmq

Version:

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

1,729 lines (1,525 loc) 1.6 MB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.busmq = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ var crypto = window.crypto || window.msCrypto; module.exports = { randomBytes: function(size) { size = size * 2; var array = new Uint8Array(size); crypto.getRandomValues(array); array.toString = function(enc) { if (enc === 'hex') { var result = []; for (var i = 0; i < array.length; i++) { result.push(array[i].toString(16)); } return result.join('').substr(0, size); } else { return array.toString(enc); } }; return array; } }; },{}],2:[function(require,module,exports){ },{}],3:[function(require,module,exports){ var events = require('events'); var util = require('util'); /** * Module dependencies. */ var global = (function() { return this; })(); /** * WebSocket constructor. */ var WebSocket = global.WebSocket || global.MozWebSocket; /** * Module exports. */ module.exports = WebSocket ? ws : null; /** * WebSocket constructor. * * The third `opts` options object gets ignored in web browsers, since it's * non-standard, and throws a TypeError if passed to the constructor. * See: https://github.com/einaros/ws/issues/227 * * @param {String} uri * @param {Array} protocols (optional) * @param {Object) opts (optional) * @api public */ function ws(uri, protocols, opts) { events.EventEmitter.call(this); var instance; if (protocols) { instance = new WebSocket(uri, protocols); } else { instance = new WebSocket(uri); } instance.binaryType = 'arraybuffer'; this.instance = instance; this.__defineGetter__('readyState', function() { return instance.readyState; }); this.__defineGetter__('url', function() { return instance.url; }); this.__defineGetter__('bufferedAmount', function() { return instance.bufferedAmount; }); this.__defineGetter__('protocol', function() { return instance.bufferedAmount; }); this.__defineGetter__('binaryType', function() { return instance.binaryType; }); } util.inherits(ws, events.EventEmitter); ws.prototype.on = function(event, cb) { events.EventEmitter.prototype.on.apply(this, arguments); if (['message', 'open', 'close', 'error'].indexOf(event) !== -1) { var $this = this; this.instance['on' + event] = function(e) { $this.emit(event, e && e.data && new TextDecoder().decode(e.data)); } } }; ws.prototype.removeListener = function(event, cb) { events.EventEmitter.prototype.removeListener.apply(this, arguments); if (['message', 'open', 'close', 'error'].indexOf(event) !== -1) { delete this.instance['on' + event]; } }; ws.prototype.send = function(data) { this.instance.send(data); }; ws.prototype.close = function() { this.instance.close(); }; ws.prototype.ping = function() { // ignore }; },{"events":109,"util":206}],4:[function(require,module,exports){ (function (process,__dirname){ var fs = require('fs'); var events = require('events'); var util = require('util'); var crypto = require('crypto'); var Logger = require('./logger'); var driver = require('./driver'); var Connection = require('./connection'); var WSPool = require('./wspool'); var Queue = require('./queue'); var Channel = require('./channel'); var Persistify = require('./persistify'); var Pubsub = require('./pubsub'); var FederationServer = require('./fedserver'); var Federate = require('./federate'); /** * A message bus. * Options: * - redis - url string of a single redis to connect to or an array of string urls to connects to. a url must have the form redis://<host>[:<port>] * - layout * 'direct': direct connection to redis instances (default) * 'sentinels': urls provided are that of sentinels (only supported by the ioredis driver) * 'cluster': urls provided belong to a redis cluster (only supported by the ioredis driver) * - redisOptions: options to pass to the redis client on instantiation (default to {}). Driver specific. * - driver: redis driver to use: 'node-redis' or 'ioredis' (defaults to 'node-redis') * - federate - object with the following options: * - server - an http/https server object to listen for incoming federate connections. if undefined then federation server will not be open. * - secret - a secret key to authenticate with the federates. this key must be shared among all federates. default is 'notsosecret'. * - path - specifies the path within the server to accept federation connections on * @constructor */ function Bus(options) { events.EventEmitter.call(this); this.id = "bus:" + crypto.randomBytes(8).toString('hex'); this.logger = new Logger(this.id); this.connections = []; this.online = false; this.eventsCallbacks = {}; this._options(options); this._startFederationServer(); if (this.options.redis.length > 0) { this._loadScripts(); } else { var _this = this; process.nextTick(function() { _this.emit('online'); }); } } util.inherits(Bus, events.EventEmitter); /** * Setup bus options * @param options options to set * @private */ Bus.prototype._options = function(options) { this.options = options || {}; if (!this.options.redis) { this.options.redis = []; } if (typeof this.options.redis === 'string') { this.options.redis = [this.options.redis]; } if (this.options.logLevel) { this.logger.level(this.options.logLevel); } if (this.options.logger) { this.withLog(this.options.logger); } this.driver = driver(this.options.driver || 'node-redis'); this.logger.isDebug() && this.logger.debug('using "'+this.driver.name+'" driver'); // this.setupDriver(this.options.driver); this.options.layout = this.options.layout || 'direct'; this.wspool = new WSPool(this, this.options.federate); }; /** * Turn debug messages to the log on or off * @param on */ Bus.prototype.debug = function(on) { this.logger.level(on ? 5 : 2); return this; }; /** * Set the logger object * @param logger */ Bus.prototype.withLog = function(logger) { this.logger.withLog(logger); return this; }; Bus.prototype._startFederationServer = function() { if (this.options.federate && this.options.federate.server) { this.fedServer = new FederationServer(this, this.options.federate); } }; /** * Load the lua scripts used for managing the queue * @private */ Bus.prototype._loadScripts = function() { this.scripts = {}; this.scriptsToLoad = 0; this.scriptsLoaded = false; this._readScript('push'); this._readScript('pop'); this._readScript('ack'); this._readScript('index'); }; /** * Read a single script to memory * @param name the script name to load * @private */ Bus.prototype._readScript = function(name) { ++this.scriptsToLoad; var _this = this; var file = __dirname + '/lua/'+name+'.lua'; fs.readFile(file, function(err, content) { if (err) { _this.logger.isDebug() && _this.logger.debug('error reading lua script ' + file + ': ' + JSON.stringify(err)); _this.emit('error', err); return; } _this.scripts[name] = {content: content.toString().trim(), name: name}; if (--_this.scriptsToLoad === 0) { _this.scriptsLoaded = true; _this._online(); } }); }; /** * Get the hash of a script by name * @param name * @returns {*} * @private */ Bus.prototype._script = function(name) { return this.scripts[name].hash; }; /** * Connect the bus to the specified urls. * Events: error, online, offline */ Bus.prototype.connect = function() { if (this.connections.length > 0) { this.emit('error', 'already connected'); return; } var _this = this; var directConnections = this.options.redis; if (this.options.layout === 'direct') { this.logger.isDebug() && this.logger.debug("bus connecting directly to redises @ " + this.options.redis); var readies = 0; directConnections.forEach(function(param, index) { // open the connections to redis createConnection(index,_this, function() { _this.logger.isDebug() && _this.logger.debug("connection "+(index+1)+" of "+ _this.options.redis.length + " is ready"); // emit the ready event only when all connections are ready if (++readies === _this.connections.length) { _this.connectionsReady = true; _this._online(); } }, function(err) { if (readies > 0 && --readies === 0) { _this.connectionsReady = false; _this._offline(err); } } ) }); } else if (this.options.layout === 'sentinels') { //create a single connection via the sentinels this.logger.isDebug() && this.logger.debug("bus connecting via sentinels @ " + this.options.redis); createConnection(0,_this, function() { _this.logger.isDebug() && _this.logger.debug("connection via sentinels is ready"); _this.connectionsReady = true; _this._online(); }, function(err) { _this._offline(err); } ) } else if (this.options.layout === 'cluster') { createConnection(-1,_this, function() { _this.logger.isDebug() && _this.logger.debug("connection to cluster is ready"); _this.connectionsReady = true; _this._online(); }, function(err) { _this._offline(err); } ) } else { _this.emit('error', 'unsupported connection layout: '+this.options.layout); return; } if (this.fedServer) { this.fedServer.listen(); } }; function createConnection(index, bus, readyCallback, endCallback) { var connection = new Connection(index, bus); bus.connections.push(connection); var _onConnectionEnd = function (err) { endCallback(err); }; var _onConnectionClose = function (err) { }; var _onConnectionError = function (err) { bus.emit('error', err); }; var _onConnectionConnect = function () { }; var _onConnectionReady = function () { readyCallback(); }; //var _onConnectionSubscribe = function (channel, count) { // if (bus.eventsCallbacks[channel]) { // bus.eventsCallbacks[channel]('subscribe', channel, count); // } //}; // //var _onConnectionUnsubscribe = function (channel, count) { // if (bus.eventsCallbacks[channel]) { // bus.eventsCallbacks[channel]('unsubscribe', channel, count); // } //}; var _onConnectionMessage = function (message) { if (bus.eventsCallbacks[message.channel]) { bus.eventsCallbacks[message.channel].forEach(function(cb) { cb('message', message.channel, message.message); }); } }; var _onConnectionCleanup = function() { connection.removeListener("end", _onConnectionEnd); connection.removeListener("close", _onConnectionClose); connection.removeListener("error", _onConnectionError); connection.removeListener("connect", _onConnectionConnect); connection.removeListener("ready", _onConnectionReady); //connection.removeListener("subscribe", _onConnectionSubscribe); //connection.removeListener("unsubscribe", _onConnectionUnsubscribe); connection.removeListener("message", _onConnectionMessage); connection.removeListener("cleanup", _onConnectionCleanup); }; connection.on("end", _onConnectionEnd); connection.on("close", _onConnectionClose); connection.on("error", _onConnectionError); connection.on("connect", _onConnectionConnect); connection.on("ready", _onConnectionReady); //connection.on("subscribe", _onConnectionSubscribe); //ioredis does not emit this //connection.on("unsubscribe", _onConnectionUnsubscribe); connection.on("message", _onConnectionMessage); connection.on('cleanup', _onConnectionCleanup); connection.connect(); } /** * Invoked when the bus is online, i.e. all connections are ready */ Bus.prototype._online = function() { if (this.connectionsReady && this.scriptsLoaded) { this.logger.isDebug() && this.logger.debug("uploading scripts to redis"); var _this = this; var scriptKeys = Object.keys(_this.scripts); var dones = scriptKeys.length * this.connections.length; // load the scripts to redis this.connections.forEach(function(connection) { scriptKeys.forEach(function(key) { var script = _this.scripts[key]; // send the script to redis var handler = function(err, resp) { if (err) { _this.emit('error', 'failed to upload script ' + script.name + ' to redis: ' + err); return; } else { //cluster will send back the hash as many times as there are nodes script.hash = Array.isArray(resp) ? resp[0] : resp; } if (--dones === 0) { _this.online = true; _this.logger.isDebug() && _this.logger.debug("bus is online"); _this.emit('online'); } }; if (_this.options.layout === 'cluster') { connection.to( 'all' ).call( 'script', 'load', script.content, handler ); } else { connection.script( 'load', script.content, handler ); } }); }); } }; /** * Invoked when the bus is offline, i.e. all connections are down */ Bus.prototype._offline = function(err) { var shouldEmit = this.isOnline(); this.online = false; this.logger.isDebug() && this.logger.debug("bus is offline"); if (shouldEmit) { this.emit('offline', err); } }; /** * Returns whether the bus is online or not * @returns {boolean|*} */ Bus.prototype.isOnline = function() { return this.online; }; /** * Get the first available connection * @returns {*} * @private */ Bus.prototype._connectionOne = function() { return this.connections[0]; }; /** * Get the next available connection * @returns {*} * @private */ Bus.prototype._connectionFor = function(key) { var sum = 0; for (var i = 0; i < key.length; ++i) { sum += key.charCodeAt(i); } var index = sum % this.connections.length; return this.connections[index]; }; /** * Get an existing connection to redis to serve the specified key. * A random connection is chosen if the key does not exist anywhere. * @param key * @param cb * @private */ Bus.prototype._connection = function(key, cb) { // most of the time the connections state will be stable so the fastest // way is to directly search a specific connection. var _this = this; var responses = this.connections.length; var _callback = function(connection) { if (connection) { cb && cb(connection); _callback = null; return; } if (--responses === 0) { // got a response from all the connections _callback = null; connection = _this._connectionFor(key); cb && cb(connection); } }; // search for the key in all the connections this.connections.forEach(function(c) { if (!c.isReady()) { _callback && _callback(null); return; } c.exists(key, function(err, resp) { if (err) { _this.emit('error', "error searching for existing key " + key + ": " + err); } if (resp === 1) { // found it _callback && _callback(c); } else { _callback && _callback(null); } }); }); }; /** * Disconnect all redis connections, close the fedserver and close all the wspool websocket connections */ Bus.prototype.disconnect = function() { this.logger.isDebug() && this.logger.debug("disconnecting"); var _this = this; this.once('offline', function() { _this.emit('cleanup'); }); this.connections.forEach(function(c) { c.disconnect(); }); this.connections = []; if (this.fedServer) { this.fedServer.close(); this.fedServer = null; } if (this.wspool) { this.wspool.close(); this.wspool = null; } }; /** * Subscribe to events on the specified channels on the provided connection * @param connection * @param channels * @param cb * @private */ Bus.prototype._subscribe = function(connection, channels, cb) { if (!Array.isArray(channels)) { channels = [channels]; } var _this = this; channels.forEach(function(channel) { _this.eventsCallbacks[channel] = _this.eventsCallbacks[channel] || []; _this.eventsCallbacks[channel].push(cb); }); connection.subscribe(channels, function(err) { if (err) { _this.emit('error', "error subscribing to channels " + channels + ": " + err); return; } channels.forEach(function(c) { cb('subscribe', c); }); }); }; /** * Unsubscribe from events on the specified channels on the provided connection * @param connection * @param channels * @param cb * @private */ Bus.prototype._unsubscribe = function(connection, channels, cb) { if (!Array.isArray(channels)) { channels = [channels]; } var _this = this; channels.forEach(function(channel) { if (_this.eventsCallbacks[channel]) { var index = _this.eventsCallbacks[channel].indexOf(cb); if (index !== -1) { _this.eventsCallbacks[channel].splice(index, 1); if (_this.eventsCallbacks[channel].length === 0) { delete _this.eventsCallbacks[channel]; } connection.unsubscribe(channels, function(err) { if (err) { _this.emit('error', "error unsubscribing from channels " + channels + ": " + err); return; } channels.forEach(function(c) { cb('unsubscribe', c); }); }); } } }); }; /** * Create a message queue. * @param name * @returns {Queue} a Queue object */ Bus.prototype.queue = function(name) { return new Queue(this, name); }; /*** * Create a bi-directional channel. * @param name the name of the channel * @param local the name of the local endpoint. default is 'client' if calling #connect and 'server' if calling #listen. * @param remote the name of the remote endpoint. default is 'server' if calling #connect and 'client' if calling #listen. * @returns {Channel} a Channel object */ Bus.prototype.channel = function(name, local, remote) { return new Channel(this, name, local, remote); }; /** * Persistify the provided object, enabling the object to be persisted to redis * @param name name of the object * @param object the object to persist * @param attributes array of property names to persist in the object. The properties will be automatically defined on the object. * @returns {*} */ Bus.prototype.persistify = function(name, object, attributes) { return Persistify(this, name, object, attributes); }; /** * Create a pubsub channel. * @param name * @returns {Pubsub} a Pubsub object */ Bus.prototype.pubsub = function(name) { return new Pubsub(this, name); }; /** * Federate an object to the specified remote bus * @param object queue, channel or persisted object * @param target string url of the remote bus to federate to * @returns {Federate} the federation object. It's possible to use the federation object only after the 'ready' event is emitted. */ Bus.prototype.federate = function(object, target) { return new Federate(object, target, this.wspool, this.options.federate); }; function create(options) { return new Bus(options); } exports = module.exports.create = create; }).call(this,require('_process'),"/lib") },{"./channel":5,"./connection":2,"./driver":6,"./federate":7,"./fedserver":2,"./logger":8,"./persistify":9,"./pubsub":10,"./queue":11,"./wspool":13,"_process":159,"crypto":1,"events":109,"fs":58,"util":206}],5:[function(require,module,exports){ 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(); } }; /** * 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; },{"./queue":11,"events":109,"util":206}],6:[function(require,module,exports){ var util = require('util'); var _url = require('url'); /** * Base driver * @param name name of the driver * @constructor */ function Driver(name, driver) { this.name = name; this.driver = driver; } Driver.prototype.direct = function(url, options) { throw new Error(this.name + ' driver does not support direct'); }; Driver.prototype.sentinels = function(urls, options) { throw new Error(this.name + ' driver does not support sentinels'); }; Driver.prototype.cluster = function(urls, options) { throw new Error(this.name + ' driver does not support cluster'); }; /** * node-redis driver * @constructor */ function NodeRedisDriver() { Driver.call(this, 'node-redis', require('redis')); } util.inherits(NodeRedisDriver, Driver); NodeRedisDriver.prototype.direct = function(url, options) { var opts = options || {}; var parsed = _url.parse(url); if (parsed.auth) { opts.auth_pass = parsed.auth; } var client = this.driver.createClient(parsed.port || 6379, parsed.hostname, opts); client.retry_delay = 1000; client.retry_backoff = 1; client.getServerInfo = function() { return client.server_info; }; return client; }; /** * ioredis driver * @constructor */ function IORedisDriver() { Driver.call(this, 'ioredis', require('ioredis')); } util.inherits(IORedisDriver, Driver); IORedisDriver.prototype._client = function(client) { client.getServerInfo = function() { return client.serverInfo }; return client; }; IORedisDriver.prototype._directOptions = function(url, options) { var opts = options || {}; var parsed = _url.parse(url); opts.port = parsed.port || 6379; opts.host = parsed.hostname; if (parsed.auth) { opts.password = parsed.auth; } opts.retryStrategy = function(times) { return 1000; }; return opts; }; IORedisDriver.prototype._sentinelsOptions = function(urls, options) { var us = Array.isArray(urls) ? urls : [urls]; var sentinels = us.map(function(u) { var parsed = _url.parse(u); return {host: parsed.hostname, port: parsed.port || 26379, password: parsed.auth}; }); var opts = options || {}; opts.sentinels = sentinels; opts.name = options.name || 'mymaster'; opts.retryStrategy = function(times) { return 1000; }; opts.sentinelRetryStrategy = function(times) { return 1000; }; return opts; }; IORedisDriver.prototype._clusterOptions = function(urls, options) { var opts = options || {}; opts.retryStrategy = function(times) { return 1000; }; opts.clusterRetryStrategy = function(times) { return 1000; }; opts.retryDelayOnClusterDown = 1000; return opts; }; IORedisDriver.prototype.direct = function(url, options) { return this._client(new this.driver(this._directOptions(url, options))); }; IORedisDriver.prototype.sentinels = function(urls, options) { return this._client(new this.driver(this._sentinelsOptions(urls, options))); }; IORedisDriver.prototype.cluster = function(urls, options) { var us = Array.isArray(urls) ? urls : [urls]; var nodes = us.map(function(u) { var parsed = _url.parse(u); return {host: parsed.hostname, port: parsed.port || 26379, password: parsed.auth_pass}; }); return this._client(new this.driver.Cluster(nodes, this._clusterOptions(options))); }; // //function getNodeRedisDriver() { // // var driver = require('redis'); // // function normalizeOptions(options) { // return options || {}; // } // // function normalizeClient(client) { // client.retry_delay = 1000; // client.retry_backoff = 1; // //normalize to getServerInfo() // client.getServerInfo = function() { // return client.server_info // }; // return client; // } // // return { // name: 'node-redis', // direct: function(url, options) { // var opts = normalizeOptions(options); // var parsed = _url.parse(url); // if (parsed.auth) { // opts.auth_pass = parsed.auth; // } // return normalizeClient(driver.createClient(parsed.port || 6379, parsed.hostname, opts)); // }, // sentinels: function(urls, options) { // throw new Error('the node-redis driver does not support sentinels'); // }, // cluster: function(urls, options) { // throw new Error('the node-redis driver does not support redis clusters'); // } // } //} // //function getIORedisDriver() { // // var driver = require('ioredis'); // // function normalizeDirectOptions(url, options) { // var opts = options || {}; // // var parsed = _url.parse(url); // opts.port = parsed.port || 6379; // opts.host = parsed.hostname; // if (parsed.auth) { // opts.password = parsed.auth; // } // opts.retryStrategy = function(times) { // return 1000; // }; // return opts; // } // // function normalizeSentinelsOptions(urls, options) { // var us = Array.isArray(urls) ? urls : [urls]; // var sentinels = us.map(function(u) { // var parsed = _url.parse(u); // return {host: parsed.hostname, port: parsed.port || 26379, password: parsed.auth}; // }); // // var opts = options || {}; // opts.sentinels = sentinels; // opts.name = options.name || 'mymaster'; // opts.retryStrategy = function(times) { // return 1000; // }; // opts.sentinelRetryStrategy = function(times) { // return 1000; // }; // return opts; // } // // // function normalizeClusterOptions(options) { // var opts = options || {}; // opts.retryStrategy = function(times) { // return 1000; // }; // opts.clusterRetryStrategy = function(times) { // return 1000; // }; // opts.retryDelayOnClusterDown = 1000; // return opts; // } // // function normalizeClient(client) { // //normalize to getServerInfo() // client.getServerInfo = function() { // return client.serverInfo // }; // return client; // } // // return { // name: 'ioredis', // direct: function(url, options) { // return normalizeClient(new driver(normalizeDirectOptions(url, options))); // }, // sentinels: function(urls, options) { // return normalizeClient(new driver(normalizeSentinelsOptions(urls, options))); // }, // cluster: function(urls, options) { // var us = Array.isArray(urls) ? urls : [urls]; // var nodes = us.map(function(u) { // var parsed = _url.parse(u); // return {host: parsed.hostname, port: parsed.port || 26379, password: parsed.auth_pass}; // }); // return normalizeClient(new driver.Cluster(nodes, normalizeClusterOptions(options))); // } // } //} function driver(name) { switch (name) { case 'node-redis': return new NodeRedisDriver(); case 'ioredis': return new IORedisDriver(); default: throw 'unsupported driver: ' + name; } } exports = module.exports = driver; },{"ioredis":123,"redis":33,"url":202,"util":206}],7:[function(require,module,exports){ var events = require('events'); var util = require('util'); var url = require('url'); var WebSocketStream = require('websocket-stream'); var dnode = require('dnode'); function _noop() {} /** * Federate * @param object * @param target * @param wspool * @param options * @constructor */ function Federate(object, target, wspool, options) { events.EventEmitter.call(this); this.object = object; this.target = target; this.reconnecting = false; this.channelpool = wspool; this.state = {}; this._options(options); this.queue = []; this._attachWs(false); } util.inherits(Federate, events.EventEmitter); /** * Setup Federate options * @param options options to set * @private */ Federate.prototype._options = function(options) { this.options = util._extend({}, options); this._setupMethods(); }; /** * Set the methods that need to be federated. * if not specifically specified then federate all the public methods. * @private */ Federate.prototype._setupMethods = function() { var _this = this; if (this.object.federatedMethods) { this.options.methods = this.object.federatedMethods; } else { var methods = ['on', 'once'].concat(Object.getOwnPropertyNames(_this.object.constructor.prototype)); this.options.methods = methods.filter(function(prop) { return typeof _this.object[prop] === 'function' && prop.indexOf('_') !== 0 && prop !== 'constructor'; }); } this._setupStateTracker(); }; /** * Some methods need tracking to be invoked again if the underlying websocket is dropped and reconnects again * @private */ Federate.prototype._setupStateTracker = function() { var _this = this; // always track event emitter methods this.state['on'] = { events: {}, save: function(args) { this.events[args[0]] = args; }, args: function() { // return an array of args of all the listener events that are registered return Object.keys(this.events).map(function(e) {return this.events[e];}.bind(this)); } }; this.state['removeListener'] = { unsave: function(args) { delete _this.state['on'].events[args[0]]; } }; // also track custom methods from the object if (this.object._federationState) { var state = this.object._federationState(); state.forEach(function(pair) { // save the state when this method is invoked _this.state[pair.save] = {save: pair.save}; // turn off the state if this method is invoked _this.state[pair.unsave] = {unsave: typeof pair.unsave === 'function' ? pair.unsave : pair.save}; }); } }; Federate.prototype._attachWs = function(reconnect) { var _this = this; this.channelpool.get(this.target, function(err, ws) { if (err) { if (err === 'unauthorized') { _this.emit('unauthorized'); } else { _this.emit('error', err); } return; } _this._to(ws, reconnect); }); }; /** * Start federating the object through the specified websocket * @param channel a channel to federate over * @param reconnect whether this is a reconnection for the same object * @returns {Federate} */ Federate.prototype._to = function(channel, reconnect) { if (this.channel) { this.emit('error', 'already federating to ' + this.object.id); } var _this = this; var _onWsMessage = function(msg) { if (msg.slice(0,5).toString() === 'ready') { _this._federate(reconnect); } }; var _onWsUnexpectedResponse = function(req, res) { // unauthorized means wrong secret key var reason = 'unexpected response'; var error; if (res.statusCode === 401) { reason = 'unauthorized'; _this.emit(reason); } else { error = 'federation received unexpected response ' + res.statusCode; } _onWsShutdown(reason, error); }; var _onWsError = function(error) { _this.object.logger.isDebug() && _this.object.logger.debug('federation transport error: ' + error); _onWsShutdown('error', error); }; var _onWsClose = function(message) { _this.object.logger.isDebug() && _this.object.logger.debug('federation transport closed: ' + message); _onWsShutdown('unexpected closed', 'closed due to ' + message); }; var _onWsShutdown = function(reason, error) { _this.object.logger.isDebug() && _this.object.logger.debug('federation transport shutting down: ' + reason + (error ? '('+error+')' : '')); channel.removeListener('message', _onWsMessage); channel.removeListener('unexpected-response', _onWsUnexpectedResponse); channel.removeListener('error', _onWsError); channel.removeListener('close', _onWsClose); channel.removeListener('shutdown', _onWsShutdown); if (_this.channelStream) { _this.channelStream.unpipe(); _this.channelStream = null; } if (_this.dnode) { _this.dnode.emit('shutdown'); _this.dnode = null; } if (_this.channel) { _this.channel.close(); _this.channel = null; } if (error) { _this._reconnect(reason); } else { _this.emit('close'); } }; channel.once('message', _onWsMessage); channel.on('unexpected-response', _onWsUnexpectedResponse); channel.on('error', _onWsError); channel.on('close', _onWsClose); channel.on('shutdown', _onWsShutdown); this.channel = channel; var parsedTarget = url.parse(channel.url, true); delete parsedTarget.search; delete parsedTarget.query.secret; this.target = url.format(parsedTarget); this.object.logger.isDebug() && this.object.logger.debug('starting federation to ' + this.target); this._sendCreationMessage(); return this; }; /** * Send the server the object creation message. federation will start once the server sends back the 'ready' message. * @private */ Federate.prototype._sendCreationMessage = function() { var msg = JSON.stringify(this._objectCreationMessage(this.object)); this.object.logger.isDebug() && this.object.logger.debug('sending federation creation message ' + msg); this.channel.send(msg); }; /** * Reconnect this federation object over a new transport */ Federate.prototype._reconnect = function(reason) { if (this.channel) { this.emit('error', 'cannot reconnect - already connected'); return; } if (!this.reconnecting) { this.reconnecting = true; this.emit('reconnecting', reason); } this._attachWs(true); }; /** * Stop federating the object and close the channel */ Federate.prototype.close = function() { if (this.channel) { this.channel.emit('shutdown', 'requested shutdown'); } }; Federate.prototype._objectCreationMessage = function(object) { var type = object.type; if (!type) { if (object._p) { type = 'persistify'; } } var message = {type: type, methods: this.options.methods}; switch(type) { case 'queue': message.args = [object.name]; break; case 'channel': message.args = [object.name, object.local, object.remote]; break; case 'persistify': message.args = [object._p.name, {}, object._p.attributes]; break; case 'pubsub': message.args = [object.name]; break; } return message; }; /** * Federate the object methods * @private */ Federate.prototype._federate = function(reconnect) { var _this = this; function callRemote(method, args) { _this.remote[method].call(_this.remote, args); } // federate the methods to the remote endpoint. var methods = _this.options.methods; methods.forEach(function(method) { _this.object[method] = function() { // check if this is a state saving/unsaving method if (_this.state[method]) { // if we should save the state, that is save the args and invoke the method again on reconnect if (_this.state[method].save) { if (typeof _this.state[method].save === 'function') { _this.state[method].save(arguments); } else { _this.state[method].args = arguments; } } else { // this method clears the state that was set by another method if (typeof _this.state[method].unsave === 'function') { _this.state[method].unsave(arguments); } else { delete _this.state[_this.state[method].unsave].args; } } } // store the method calls until we are online if (!_this.remote) { _this.queue.push({method: method, args: arguments}); return; } callRemote(method, arguments); } }); this.dnode = dnode(null, {weak: false}); var _onDnodeRemote = function(remote) { _this.remote = remote; if (reconnect) { // if we need to restore the remote object state Object.keys(_this.state).forEach(function(m) { if (_this.state[m].args) { if (typeof _this.state[m].args === 'function') { var args = _this.state[m].args(); if (!Array.isArray(args)) { args = [args]; } args.forEach(function(a) { callRemote(m, a); }); } else { callRemote(m, _this.state[m].args); } } }); _this.reconnecting = false; _this.emit('reconnected', _this.object); } else { _this.emit('ready', _this.object); } // call any methods that are pending because we were not connected _this.queue.forEach(function(e) { callRemote(e.method, e.args); }); _this.queue = []; }; var _onDnodeError = function(err) { _this.emit('error', err); }; var _onDnodeFail = function(err) { _this.emit('fail', err); }; var _onDnodeShutdown = function() { _this.dnode.removeListener('remote', _onDnodeRemote); _this.dnode.removeListener('shutdown', _onDnodeShutdown); _this.dnode.removeListener('error', _onDnodeError); _this.dnode.removeListener('fail', _onDnodeFail); _this.dnode.end(); _this.remote = null; }; this.dnode.on('remote', _onDnodeRemote); this.dnode.on('shutdown', _onDnodeShutdown); this.dnode.on('error', _onDnodeError); this.dnode.on('fail', _onDnodeFail); // wrap the websocket with a stream this.channelStream = WebSocketStream(this.channel); this.channelStream.on('error', _noop); // pipe the stream through dnode this.channelStream.pipe(this.dnode).pipe(this.channelStream); }; module.exports = exports = Federate; },{"dnode":87,"events":109,"url":202,"util":206,"websocket-stream":209}],8:[function(require,module,exports){ /** * Logger. * @param tag a tag to use on every message * @param logger the underlying logger instance * @constructor */ function Logger(tag, logger) { this._tag = tag; this._logger = logger; this._level = 1; // support the various logging functions var _this = this; ['log', 'trace', 'debug', 'info', 'warn', 'warning', 'error', 'fatal', 'exception'].forEach(function(f) { _this[f] = function() { if (_this._logger && LEVEL[f] <= _this._level) { var message; // add the tag to the message for (var i = 0; i < arguments.length; ++i) { if (typeof arguments[i] === 'string') { arguments[i] = "["+_this._tag+"] " + arguments[i]; message = arguments[i]; break; } } var method = _this._logger[f] || _this._logger['log']; method.apply(_this._logger, arguments); } } }); } var LEVEL = Logger.prototype.LEVEL = { log: -1, exception: 0, fatal: 0, error: 1, warning: 2, warn: 2, info: 3, debug: 4, trace: 5 }; /** * Set or get the log level */ Log