node-busmq
Version:
A high performance, highly-available and scalable, message bus and queueing system for node.js backed by Redis
246 lines (212 loc) • 7.13 kB
JavaScript
var events = require('events');
var util = require('util');
var url = require('url');
var WebSocket = require('ws');
var WebSocketStream = require('websocket-stream');
var dnode = require('@capriza/dnode');
var WSMux = require('./wsmux');
function _noop() {}
/**
* FederationServer
* @param bus
* @param options
* @constructor
*/
function FederationServer(bus, options) {
events.EventEmitter.call(this);
this.bus = bus;
this.logger = bus.logger.withTag(bus.id + ':fedserver');
this._options(options);
}
util.inherits(FederationServer, events.EventEmitter);
/**
* Setup FederationServer options
* @param options options to set
* @private
*/
FederationServer.prototype._options = function(options) {
this.options = util._extend({}, options);
this.options.secret = this.options.secret || 'notsosecret';
this.options.path = this.options.path || '/';
};
/**
* Start the federation server
*/
FederationServer.prototype.listen = function() {
if (this.listening) {
return;
}
if (this.options.server) {
var _this = this;
var verifyClient = this.options.secret;
if (typeof verifyClient !== 'function') {
verifyClient = this._verifyClient.bind(this);
}
this.wss = new WebSocket.Server({server: this.options.server, verifyClient: verifyClient, path: this.options.path});
var _onWssConnection = function(ws) {
_this._onConnection(ws);
};
var _onWssListening = function() {
_this.logger.isDebug() && _this.logger.debug('websocket server is listening');
_this.listening = true;
_this.emit('listening');
};
var _onWssError = function(err) {
_this.logger.isDebug() && _this.logger.debug('error on websocket server: ' + JSON.stringify(err));
_this.emit('error', err);
};
var _onWssShutdown = function() {
_this.wss.removeListener('connection', _onWssConnection);
_onWssConnection = undefined;
_this.wss.removeListener('listening', _onWssListening);
_onWssListening = undefined;
_this.wss.removeListener('error', _onWssError);
_onWssError = undefined;
_this.wss.removeListener('shutdown', _onWssShutdown);
_onWssShutdown = undefined;
_this.wss.close();
_this.wss = null;
};
this.wss.on('connection', _onWssConnection);
this.wss.on('listening', _onWssListening);
this.wss.on('error', _onWssError);
this.wss.on('shutdown', _onWssShutdown);
}
};
/**
* Close the federation server
*/
FederationServer.prototype.close = function() {
if (!this.listening) {
return;
}
this.listening = false;
this.wss.emit('shutdown');
};
/**
* Handle a new connection
* @param ws the new connection
* @private
*/
FederationServer.prototype._onConnection = function(ws) {
this.logger.isDebug() && this.logger.debug('new federate client connection');
var _this = this;
var onWsClose = function(code) {
shutdown('websocket closed: ' + code);
};
var onWsError = function(err) {
shutdown('error on websocket: ' + JSON.stringify(err));
};
var onChannel = function(channel) {
_this._onChannel(channel);
};
var shutdown = function(msg) {
_this.logger.isDebug() && _this.logger.debug(msg);
wsmux.removeListener('close', onWsClose);
onWsClose = undefined;
wsmux.removeListener('error', onWsError);
onWsError = undefined;
wsmux.removeListener('channel', onChannel);
onChannel = undefined;
wsmux.close();
shutdown = undefined;
wsmux = undefined;
};
var wsmux = new WSMux(ws, {mask: false});
wsmux.on('close', onWsClose);
wsmux.on('error', onWsError);
wsmux.on('channel', onChannel);
};
/**
* Handle a new channel
* @param channel the new channel
* @private
*/
FederationServer.prototype._onChannel = function(channel) {
var object;
var d;
var _this = this;
var onChannelMessage = function(msg) {
_this.logger.isDebug() && _this.logger.debug('received message: ' + msg);
try {
msg = JSON.parse(msg);
} catch (e) {
_this.emit('error', 'error parsing incoming channel message ' + msg + ': ' + e.message);
return;
}
if (!_this.bus[msg.type]) {
_this.emit('error', 'received unknown message type in incoming channel message: ' + JSON.stringify(msg));
return;
}
object = _this.bus[msg.type].apply(_this.bus, msg.args);
d = _this._federate(object, msg.methods, channel);
};
var onChannelClose = function() {
shutdown('federate client connection closed');
};
var onChannelError = function(err) {
shutdown('federate client error: ' + JSON.stringify(err));
};
var shutdown = function(msg) {
_this.logger.isDebug() && _this.logger.debug('['+(object?object.id:'uninitialized')+'] ' + msg);
channel.removeListener('message', onChannelMessage);
onChannelMessage = undefined;
channel.removeListener('close', onChannelClose);
onChannelClose = undefined;
channel.removeListener('error', onChannelError);
onChannelError = undefined;
shutdown = undefined;
channel = undefined;
object && object.detach && object.detach();
object && object.unpersist && object.unpersist();
d.end();
d._federationStream.unpipe();
delete d._federationStream;
d = undefined;
};
channel.once('message', onChannelMessage);
channel.on('close', onChannelClose);
channel.on('error', onChannelError);
};
/**
* Hookup all the needed methods of the object to be served remotely
* @param object
* @param methods
* @param channel
* @private
*/
FederationServer.prototype._federate = function(object, methods, channel) {
this.logger.isDebug() && this.logger.debug('creating federated object ' + object.id);
var federatable = {};
methods.forEach(function(method) {
federatable[method] = function(args) {
// the arguments arrive as a hash, so make them into an array
var _args = Object.keys(args).map(function(k) {return args[k]});
// invoke the real object method
object[method].apply(object, _args);
}
});
// setup dnode to receive the methods of the object
var d = dnode(federatable, {weak: false});
// tell the client that we are ready
channel.send('ready');
// start streaming rpc
var channelStream = WebSocketStream(channel);
channelStream.on('error', _noop);
channelStream.pipe(d).pipe(channelStream);
d._federationStream = channelStream;
return d;
};
FederationServer.prototype._endFederation = function(msg, object, d) {
};
/**
* Accept or reject a connecting web socket. the connecting web socket must contain a valid secret query param
* @param info
* @returns {*|boolean}
* @private
*/
FederationServer.prototype._verifyClient = function(info) {
var parsed = url.parse(info.req.url, true);
return parsed.query && (parsed.query.secret === this.options.secret);
};
module.exports = exports = FederationServer;