UNPKG

illyria

Version:

The next generation Illyria RPC SDK for node.js.

416 lines (349 loc) 10.3 kB
/** * XadillaX created at 2015-03-03 16:05:08 * * Copyright (c) 2015 Huaban.com, all rights * reserved */ var net = require("net"); var util = require("util"); var EventEmitter = require("eventemitter2").EventEmitter2; var msgpack = require("msgpack"); var helper = require("./helper"); var emptyFunc = function(){}; /** * _formatSendBody * @param {*} _body the body to be formatted * @return {*} the body formatted */ var _formatSendBody = function(_body) { if(undefined === _body || null === _body) { return null; } if(_body instanceof Date || _body instanceof Error) { return _body; } if(util.isArray(_body)) { return _body.map(function(body) { return _formatSendBody(body); }); } if(typeof _body === "object") { var res = {}; for(var key in _body) { if(!_body.hasOwnProperty(key)) continue; res[key] = _formatSendBody(_body[key]); } return res; } if(typeof _body === "number" && (isNaN(_body) || !isFinite(_body))) return null; return _body; }; /** * ISocket * @param {Socket} [socket] the socket object * @param {Object} options the options * @constructor */ var ISocket = function(socket, options) { if(!options) { options = socket || {}; socket = new net.Socket(options); } this.socket = socket; this.connected = options.connected || socket.writable && socket.readable || false; this.retry = { retries: 0, max: options.maxRetries || 10, interval: options.retryInterval || 5000, wait: options.retryInterval || 5000 }; this.$reconnect = options.reconnect || false; this.$options = options; this.$buff = null; this.$msg = null; EventEmitter.call(this, { delimiter: "::", wildcard: true, maxListeners: options.maxListeners || 10 }); this._setup(); }; util.inherits(ISocket, EventEmitter); /** * send a message into this socket * @param {Array} event the event identity array * @param {Object} [params] the data message * @param {Function} [callback] the callback function */ ISocket.prototype.send = function(event, params, callback) { if(typeof params === "function") { callback = params; params = undefined; } if(undefined === callback) { callback = emptyFunc; } if(!this.socket || !this.connected) { return this.emit("error", new Error("ISocket: sending on a bad socket.")); } var message = [].concat(event); if(undefined !== params) { // fix a bug that params has some value like `NaN`. // and this value won't be parsed by other clients or servers such // as Elixir... params = _formatSendBody(params); message.push(params); } message = msgpack.pack(message); var buff = new Buffer(4 + message.length); buff.writeUInt32BE(message.length, 0, true); message.copy(buff, 4); this.socket.write(buff, callback); }; /** * send a cast reply message * @param {Array} event the event identity array * @param {Function} callback the callback function */ ISocket.prototype.replyCast = function(event, callback) { this.send(event.concat([ "noreply" ]), callback); }; /** * send a successful message * @param {Array} event the event identity array * @param {Object} params the data message * @param {Function} callback the callback function */ ISocket.prototype.success = function(event, params, callback) { this.send(event.concat([ "reply" ]), params, callback); }; /** * send an error message * @param {Array} event the event identity array * @param {Object} error the error data * @param {Function} callback the callback function */ ISocket.prototype.error = function(event, error, callback) { this.send(event.concat([ "error" ]), error, callback); }; /** * listen for an event * @param {Array} event the event identity array * @param {Function} listener the processor to process this event */ ISocket.prototype.data = function(event, listener) { this.on([ "data" ].concat(event), listener); }; /** * unlisten for an event * @param {Array} event the event identity array * @param {Function} listener the processor to process this event */ ISocket.prototype.undata = function(event, listener) { this.off([ "data" ].concat(event), listener); }; /** * listen for an event once * @param {Array} event the event identity array * @param {Function} listener the processor to process this event */ ISocket.prototype.dataOnce = function(event, listener) { this.once([ "data" ].concat(event), listener); }; /** * set idle/timeout timer * @param {Number} time how often to emit idle */ ISocket.prototype.setIdle = function(time) { this.socket.setTimeout(time); this._timeout = time; }; /** * destroy this object */ ISocket.prototype.destroy = function() { this.removeAllListeners(); if(this.socket) { try { this.socket.end(); this.socket.destroy(); } catch(e) { // do nothing... } } this.$msg = null; this.$buff = null; this.emit("destroy"); }; /** * close the underlying socket, recommend you call destroy after */ ISocket.prototype.end = function() { this.connected = false; if(this.socket) { try { this.socket.end(); this.emit("close"); } catch(e) { this.emit("error", e); return; } this.socket = null; } }; /** * connect * @param {...} arguments */ ISocket.prototype.connect = function(/** ... */) { var args = Array.prototype.slice.call(arguments); var self = this; var callback, host, port; args.forEach(function(arg) { switch(typeof arg) { case "number": port = arg; break; case "string": host = arg; break; case "function": callback = arg; break; default: self.emit("error", new Error("Bad argument to connect")); break; } }); this.port = port || this.port; this.host = host || this.host || "127.0.0.1"; args = this.port ? [ this.port, this.host ] : [ this.host ]; var finish = function() { self.retry.waiting = false; self.retry.retries = 0; if(callback) callback.apply(null, arguments); }; args.push(finish); var errHandlers = this.listeners("error"); if(errHandlers.length > 0) { this.socket.on("error", errHandlers[errHandlers.length - 1]); } this.connected = true; this.socket.connect.apply(this.socket, args); }; /** * try for reconnect */ ISocket.prototype.reconnect = function() { var self = this; if(self.retry.waiting) return; var doReconn = function() { if(self.socket) { self.socket.end(); self.socket.destroy(); self.socket.removeAllListeners(); self.socket = null; } self.socket = new net.Socket(self.$options); self.socket.once("connect", function() { self.retry.waiting = false; self.retry.retries = 0; }); self._setup(); self.connect(); }; var tryReconn = function() { if(self.retry.retries >= self.retry.max) { return self.emit("error", new Error("Did not reconnect after maximum retries: " + self.retry.max + ".")); } doReconn(); }; this.retry.waiting = true; this.retry.wait = this.retry.interval * Math.pow(2, this.retry.retries); this.retry.retries++; self.emit("tryReconnect", this.retry.wait); setTimeout(tryReconn, this.retry.wait); }; /** * set keep alive * @param {...} arguments */ ISocket.prototype.setKeepAlive = function(/** ... */) { this.socket.setKeepAlive.apply(this.socket, arguments); }; /** * _onData * @param {Buffer} buf the buffer received */ ISocket.prototype._onData = function(buf) { var event, data, parsed; if(this.$buff) { buf = Buffer.concat([ this.$buff, buf ], this.$buff.length + buf.length); } var msg = this.$msg || {}; while(buf.length > 0) { // not finish yet, it will return and wait for next buffer if(!helper.parseBuffer(buf, msg)) { this.$buff = buf; this.$msg = msg; return; } try { parsed = msgpack.unpack(msg.data); if([ "error", "reply", "noreply" ].indexOf(parsed[1]) >= 0) { event = String(parsed.shift()); data = parsed; } else { data = parsed.pop(); event = parsed; } } catch(err) { // Don't do anything, assume that the message was only partially // received } this.emit([ "data" ].concat(event), data); buf = buf.slice(msg._offset); msg = {}; } this.$buff = null; this.$msg = null; }; /** * setup event listeners */ ISocket.prototype._setup = function() { var self = this; // Event: new connection connected this.socket.on("connect", function() { self.emit("connected"); }); // Event: closed this.socket.on("close", function(hadError) { self.connected = false; self.$buff = null; self.$msg = null; if(hadError) { self.emit("close", hadError, arguments[1]); } else { self.emit("close"); } if(self.socket && self.$reconnect) self.reconnect(); }); // Event: data received this.socket.on("data", this._onData.bind(this)); var onError = function(error) { self.connected = false; self.$buff = null; self.$msg = null; self.retry.waiting = false; if(self.$reconnect) { self.reconnect(); } else { self.emit("error", error || new Error("Unknown error occurred in ISocket")); } }; // Event: error occurred this.socket.on("error", onError); // Event: timeout this.socket.on("timeout", function() { self.emit("idle"); if(self.$timeout) self.socket.setTimeout(self.$timeout); }); }; module.exports = ISocket;