UNPKG

dnode-tarantula

Version:

asynchronous rpc system for node.js, bi-direction and poly-direction communication...

400 lines (368 loc) 12.2 kB
var net = require('net'); var proto = require('dnode-protocol'); var EventEmitter = require('events').EventEmitter; var util = require('util'); var parser = require('./parser'); var shortid = require('shortid'); var memoryStore = require('../stores/memory'); var logger = require('./logger'); var DEFAULT = { PORT: 5000, DELIMITER: "\0", }; var Client = function(options) { var self = this; this.id = shortid.generate(); this.client = options.client; this._remote = null; this.first = true; this.authenticated = options.authenticated || false; this.buffer = ''; this.store = options.store; this.hooks = {}; this.pingInterval = options.pingInterval || 10000; this.pingTimeout = options.pingTimeout || this.pingInterval*1.2; this.log = options.log || new logger(); this.server = options.server; this.api = options.api; this.api.$.ping = this.ping.bind(this); this.store.subscribe(this.id); this.dnode = proto(this.api); this.client.on('data', this.onData.bind(this)); this.client.on('end', this.server.disconnection.bind(this.server, this)) this.dnode.on('request', this._write.bind(this)); this.dnode.on('remote', this._onRemote.bind(this)); this.dnode.start(); this.initPing(); } util.inherits(Client, EventEmitter); Client.prototype.remote = function() { args = Array.prototype.slice.call(arguments, 0); return {exec: this._callRemote.bind(this, args)}; } Client.prototype._callRemote = function(args, clientId, method) { if(clientId === this.id) { this._remote[method].apply(undefined, args); } else { scrub = this.dnode.scrubber.scrub(args); payload = { method: method, arguments: scrub.arguments, callbacks: scrub.callbacks, links: scrub.links, client_id: this.id } this.store.publish(clientId, payload); } } Client.prototype.initPing = function() { var self = this; this.on('ping:reply', function() { clearTimeout(self.pingTimeout); }); setInterval(function() { self.ping(function() { self.pingTimeout = setTimeout(function() { self.emit('ping:timeout', self); }, this.pingTimeout); }); }, self.pingInterval); } Client.prototype.ping = function(callback) { this._write("PING"); this.emit('ping', this); } Client.prototype.handlePingAnswer = function() { this.emit('ping:reply', this); this.store.ttl(this.id); } Client.prototype.event = function(name) { this._write({ type: 'event', name: name, arguments: Array.prototype.slice.call(arguments, 1), }); } Client.prototype.onData = function(chunk) { var self = this; if(!this.authenticated) { return; } parser.chunk(this, chunk, function(data) { this.parse(data, function(err, isEvent, data, isString) { if(isString && data == "PONG") { self.handlePingAnswer(); return; } if(isEvent) { self._event(data.name, data.args); } else { self.hookCheck(data, function(err, hook) { if(err) { throw new Error(err); } if(hook) { self.store.publish(hook.clientId, data); self.hooks[hook.name] = null; delete self.hooks[hook.name]; self.log.debug("publishing data to client "+hook.clientId+":", data); } else { self.dnode.handle(data); } }); } }); }.bind(this)); this.log.debug('got data from client: ', ""+chunk); } Client.prototype.update = function() { this.first = false; this.dnode.start(); } Client.prototype.ready = function() { this._write({ type: 'event', name: 'ready', arguments: [], }); } Client.prototype.parse = function(data, callback) { try { data = JSON.parse(data); } catch (e) { data = data; callback(null, false, data, true); return; } if(data && data.type && data.type === 'event') { callback(null, true, data, false) } else { callback(null, false, data, false); } } Client.prototype._event = function(name, args, callback) { args = args || []; this.emit(name, args); callback && callback() } Client.prototype._onRemote = function(remote) { if(this.first) { this._remote = remote; this._remote.$._setId(this.id); this.emit('remote', remote); } } Client.prototype._write = function(data) { if('object' == typeof data) { data = JSON.stringify(data); } if(this.client.writable) { this.client.write(data+DEFAULT.DELIMITER); this.log.debug("writing data to client:", data); } } Client.prototype.hookCheck = function(data, callback) { if(this.hooks[data.method]) { callback(null, this.hooks[data.method]); } else { callback(null, null); } } Client.prototype.hook = function(id, data, callback) { if(Object.keys(data.callbacks).length > 0) { for(var i in data.callbacks) { this.hooks[i] = { name: i, clientId: id } } } callback && callback(); } Client.prototype.disconnect = function(msg, callback) { if(msg && 'function' !== typeof msg) { this._write("ERR:"+msg); } this.client.end(); if('function' == typeof msg) { callback = msg; } callback && 'function' == typeof callback && callback(); this.log.info('disconnected client'); } var Server = function(api, options) { var options = options || {}; var api = api || {}; var server = new net.Server(); var store = options.store || memoryStore; if('function' == typeof api) { api.prototype.$ = {}; api.prototype.$.ids = this.ids.bind(this); api.prototype.$._changeId = this._changeId.bind(this); } else { api.$ = {}; api.$.ids = this.ids.bind(this); api.$._changeId = this._changeId.bind(this); api.$.update = this.update.bind(this); } this.buffer = ''; this.server = server; this.id = options.id || shortid.generate(); this.pingInterval = options.pingInterval || 10000; this.store = new store({id: this.id, pingInterval: this.pingInterval}); this.auth = options.auth || null; this.log = new logger(options.log || true); this.store.on('request', this.handleStoreRequest.bind(this)); server.on('connection', this.onConnection.bind(this, api)); server.listen(options.port || DEFAULT.PORT, options.host); this.log.info("server started"); } util.inherits(Server, EventEmitter); Server.prototype.onConnection = function(api, client) { var self = this; var api = util._extend({}, api); if(this.auth !== null) { client.once('data', this.authListener.bind(this, api, client)); client.write("AUTH:require"+DEFAULT.DELIMITER); } else { this.setClient({ client: client, server: this, api: api, store: this.store, log: this.log, authenticated: true }, function(remote, client) { self.emit('connection', remote, client, client.api); }); } this.log.info("client connected"); } Server.prototype.authListener = function(api, client, chunk) { var self = this; parser.chunk(self, chunk, function(data) { if(data.indexOf("AUTH:") == 0) { self.auth(data.slice(5), function(err, success, cb) { if(err) { throw new Error(err); } if(!success) { self.disconnection("Not authenticated", client); } else { if(cb && 'function' == typeof cb) { cb(null, true); } client.write("AUTH:success"+DEFAULT.DELIMITER); self.setClient({ client: client, server: self, api: api, store: self.store, log: self.log, authenticated: true }, function(remote, client) { self.emit('connection', remote, client, client.api); }); } }); } }); } Server.prototype.setClient = function(options, callback) { client = new Client(options); client.on('remote', function(remote) { callback(remote, client); }); this.store.set(client); } Server.prototype.close = function(callback) { this.server.close(callback); this.log.info("closing server"); } Server.prototype.handleStoreRequest = function(payload, id, data) { var self = this; this.store.get(id, function(err, client) { if(err) { if(self.listenerCount(self, 'error') < 1) { throw new Error(err); } else { self.emit('error', err, self); return; } } if(!payload.client_id && 'number' == typeof payload.method) { client.onData(payload); } else { client.hook(payload.client_id, payload, function() { client._write(payload); }); } }); } Server.prototype.update = function(id) { this.store.get(id, function(err, client) { client.update(); }); this.log.debug("pushing API update to remote"); } Server.prototype.ids = function(callback) { this.store.ids(function(err, ids) { callback(ids) }); } Server.prototype.broadcast = function(method) { var self = this; this.ids(function(ids) { for (var i = ids.length; i--;) { self.store.get(ids[i], function(err, client) { if (client && client.remote && client.remote[method]) { client.remote[method].apply(client.remote, Array.prototype.slice.call(arguments, 1)); } }); } }); this.log.debug('broadcasting method '+method+' to all servers'); } Server.prototype.disconnection = function(msg, client) { var self = this; if(!client) { client = msg; msg = 'disconnected'; } if(!client.id && !client.server) { //assume it`s just plain server if(msg && 'function' !== typeof msg) { client.write("ERR:"+msg+DEFAULT.DELIMITER); } client.end(); } else { client.disconnect(msg, function() { self.store.get(client.id, function(err, client) { if(err) { if(self.listenerCount(self, 'error') < 1) { throw new Error(err); } else { self.emit('error', err, self); return; } } if(client) { client.emit('disconnection', client); self.emit('disconnection', client); self.store.delete(client.id) } }); }); } } Server.prototype._changeId = function(id, newId, callback) { var self = this; if(id !== newId) { this.store.changeId(id, newId, function(err, client) { client._remote.$.updateId(newId); callback(); }); } else { callback(); } } module.exports = Server;