UNPKG

wikimedia-kad-fork

Version:

implementation of the kademlia dht for node

323 lines (274 loc) 8.5 kB
'use strict'; var inherits = require('util').inherits; var events = require('events'); var assert = require('assert'); var constants = require('./constants'); var Contact = require('./contact'); var Message = require('./message'); var Logger = require('./logger'); /** * Represents an RPC interface * @constructor * @param {Contact} contact - Your node's contact information * @param {Object} options * @param {Contact} options.replyto - Optional alternate public contact * @param {Logger} options.logger - Logger instance to use */ function RPC(contact, options) { assert(this instanceof RPC, 'Invalid instance supplied'); assert(contact instanceof Contact, 'Invalid contact was supplied'); events.EventEmitter.call(this); options = options || {}; if (options.replyto) { assert(options.replyto instanceof Contact, 'Invalid contact was supplied'); } this._hooks = {}; this._pendingCalls = {}; this._contact = options.replyto || contact; this._log = (options && options.logger) || new Logger(0); this.open(); } inherits(RPC, events.EventEmitter); /** * Open the underlying transport * @emits RPC#ready */ RPC.prototype.open = function() { var self = this; self._trigger('before:open', [], function() { self._open(function(err) { if (err) { self.emit('error', err); } else { self.emit('ready'); self._trigger('after:open'); } }); self._expirator = setInterval( self._expireCalls.bind(self), constants.T_RESPONSETIMEOUT + 5 ); }); }; /** * Close the underlying transport */ RPC.prototype.close = function() { var self = this; self._trigger('before:close', [], function() { self._close(); self._trigger('after:close'); clearInterval(self._expirator); }); }; /** * Send a RPC to the given contact * @param {Contact} contact - Delivery target for message * @param {Message} message - Message to send to target * @param {Function} callback */ RPC.prototype.send = function(contact, message, callback) { var self = this; contact = this._createContact(contact); assert(contact instanceof Contact, 'Invalid contact supplied'); assert(message instanceof Message, 'Invalid message supplied'); if (Message.isRequest(message)) { this._log.info('sending %s message to %j', message.method, contact); } else { this._log.info('replying to message to %s', message.id); } this._trigger('before:serialize', [message], function() { var serialized = message.serialize(); self._trigger('after:serialize'); self._trigger('before:send', [serialized, contact], function() { if (Message.isRequest(message) && typeof callback === 'function') { self._log.debug('queuing callback for reponse to %s', message.id); self._pendingCalls[message.id] = { timestamp: Date.now(), callback: callback }; } else { self._log.debug('not waiting on callback for message %s', message.id); } self._send(message.serialize(), contact); self._trigger('after:send'); }); }); }; /** * Handle incoming messages * @param {Buffer} buffer */ RPC.prototype.receive = function(buffer, info) { var self = this; this._trigger('before:deserialize', [buffer], function() { try { return self._doReceive(buffer, info); } catch (err) { self._log.error('failed to handle message, reason: %s', err.message); return self.emit('MESSAGE_DROP', buffer, info); } }); }; /** * Handle incoming messages, body. This is moved out of the try/catch, so that * V8 can JIT this function. */ RPC.prototype._doReceive = function(buffer, info) { var self = this; var message = Message.fromBuffer(buffer); self._trigger('after:deserialize'); var contact; if (Message.isRequest(message)) { contact = self._createContact(message.params.contact); } else { contact = self._createContact(message.result.contact); } self._trigger('before:receive', [message, contact], function() { self._execPendingCallback(message, contact); }); }; /** * Registers a "before" hook * @param {String} event - Name of the event to catch * @param {Function} handler - Event handler to register */ RPC.prototype.before = function(event, handler) { return this._register('before', event, handler); }; /** * Registers an "after" hook * @param {String} event - Name of the event to catch * @param {Function} handler - Event handler to register */ RPC.prototype.after = function(event, handler) { return this._register('after', event, handler); }; /** * Registers a middleware or "hook" in a set * @private * @param {String} time - One of "before" or "after" * @param {String} event - Name of the event to catch * @param {Function} handler - Event handler to register */ RPC.prototype._register = function(time, event, handler) { if (time !== 'before' && time !== 'after') { throw new Error('Invalid hook'); } assert(typeof event === 'string', 'Invalid event supplied'); assert(typeof handler === 'function', 'Invalid handler supplied'); // Set up the real trigger callback now that we are going to have a hook. this._trigger = this._realTrigger; var name = time + ':' + event; if (!this._hooks[name]) { this._hooks[name] = []; } this._hooks[name].push(handler); return this; }; /** * Fast path, used until some hooks are registered. */ RPC.prototype._trigger = function(name, args, complete) { return complete && complete(); }; /** * Triggers a middleware or "hook" set * @private * @param {String} event - Name of the event to trigger * @param {Array} args - Arguments to pass to event handlers * @param {Function} callback - Fired after all events are triggered */ RPC.prototype._realTrigger = function(name, args, complete) { var self = this; if (!this._hooks[name]) { return complete && complete(); } var stack = this._hooks[name]; var i = 0; function next(err) { if (err) { return self.emit('error', err); } if (i < stack.length) { var fn = stack[i]; fn.apply(self, args); i++; } else { return complete && complete(); } } args = args.concat([next]); stack[0].apply(self, args); }; /** * Create a contact object from the supplied contact information * @private * @param {Object} options */ RPC.prototype._createContact = function(options) { return new this._contact.constructor(options); }; /** * Executes the pending callback for a given message * @private * @param {Message} message - Message to handle any callbacks for * @param {Contact} contact - Contact who sent the message */ RPC.prototype._execPendingCallback = function(message, contact) { var pendingCall = this._pendingCalls[message.id]; this._log.debug('checking pending rpc callback stack for %s', message.id); if (Message.isResponse(message) && pendingCall) { pendingCall.callback(null, message); delete this._pendingCalls[message.id]; } else if (Message.isRequest(message)) { assert( constants.MESSAGE_TYPES.indexOf(message.method) !== -1, 'Message references invalid method "' + message.method + '"' ); this.emit('CONTACT_SEEN', contact); this.emit(message.method, message); } else { this._log.warn('dropping received late response to %s', message.id); } this._trigger('after:receive', []); }; /** * Expire RPCs that have not received a reply * @private */ RPC.prototype._expireCalls = function() { this._log.debug('checking pending rpc callbacks for expirations'); for (var rpcID in this._pendingCalls) { var pendingCall = this._pendingCalls[rpcID]; var timePassed = Date.now() - pendingCall.timestamp; if (timePassed > constants.T_RESPONSETIMEOUT) { this._log.warn('rpc call %s timed out', rpcID); pendingCall.callback(new Error('RPC with ID `' + rpcID + '` timed out')); delete this._pendingCalls[rpcID]; } } }; /** * Unimplemented stub, called on close() * @abstract */ /* istanbul ignore next */ RPC.prototype._close = function() {}; /** * Unimplemented stub, called on send() * @abstract * @param {Buffer} data * @param {Contact} contact */ /* istanbul ignore next */ RPC.prototype._send = function() {}; /** * Unimplemented stub, called on constructor * @abstract * @param {Function} done - callback */ RPC.prototype._open = function(done) { setImmediate(done); }; module.exports = RPC;