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
JavaScript
(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