UNPKG

endpointjs

Version:

Endpoint.js enables modules within a web application to discover and use each other, whether that be on the same web page, other browser windows and tabs, iframes, servers and web workers in a reactive way by providing robust discovery, execution and stre

439 lines (381 loc) 14.7 kB
/* * (C) 2016 * Booz Allen Hamilton, All rights reserved * Powered by InnoVision, created by the GIAT * * Endpoint.js was developed at the * National Geospatial-Intelligence Agency (NGA) in collaboration with * Booz Allen Hamilton [http://www.boozallen.com]. The government has * "unlimited rights" and is releasing this software to increase the * impact of government investments by providing developers with the * opportunity to take things in new directions. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* jshint -W097 */ /* globals __filename */ 'use strict'; var appUtils = require('../util/appUtils'), log = appUtils.getLogger(__filename), address = require('../routing/address'), constants = require('../util/constants'), isArray = require('util').isArray, xtend = require('xtend'), inherits = require('util').inherits, EventEmitter = require('events').EventEmitter; inherits(Bus, EventEmitter); module.exports = Bus; /** * The bus uses EventEmitter to create a global message bus. It also uses * the router to propagate messages to all other nodes in the * network using a controlled flooding algorithm. * https://en.wikipedia.org/wiki/Flooding_(computer_networking) * @augments EventEmitter * @fires Bus#X - where X is the global event to fire * @param {Router} routerInstance - an instance of the Router class * @param {Configuration} config - system configuration * @constructor */ function Bus(routerInstance, config) { if (!(this instanceof Bus)) { return new Bus(routerInstance, config); } this._id = config.get('instanceId'); this._sequence = 1; // Call parent Constructor EventEmitter.call(this); this.setMaxListeners(0); // This is the host information this._hosts = {}; // This allows us to look up bridges to ensure that the link is in the bridge this._linkAssociation = config.getLinkAssociation(); // Register with the router this._routerInstance = routerInstance; this._routerInstance.addHandler('bus'); this._routerInstance.on('route-available', this._handleRouteAvailable.bind(this)); this._routerInstance.on('route-change', this._handleRouteChange.bind(this)); this._routerInstance.on('route-unavailable', this._handleRouteLost.bind(this)); this._routerInstance.on('bus', this._handleBusPacket.bind(this)); } /** * When a new host is available, then register it. * @param address * @param cost * @private */ Bus.prototype._handleRouteAvailable = function(address, route) { this._hosts[address] = { address: address, linkId: route.linkId, adjacent: route.adjacent, external: route.external, sequence: -1 }; if (route.adjacent) { log.log(log.DEBUG, 'Discovered new adjacent host ' + '[id: %s]', address); } else { log.log(log.DEBUG, 'Discovered new non-adjacent host ' + '[id: %s]', address); } }; /** * When the adjacency of a route changes * @param address * @param adjacent * @private */ Bus.prototype._handleRouteChange = function(address, route) { this._hosts[address].adjacent = route.adjacent; this._hosts[address].linkId = route.linkId; }; /** * Remove the host from the registry. * @param address * @private */ Bus.prototype._handleRouteLost = function(address) { // Remove the host delete this._hosts[address]; }; /** * Read a packet from the given address * @param envelope * @param address - the immediate link we received the message from * @private */ Bus.prototype._handleBusPacket = function(packet, fromUuid, source) { // Is the immediate packet sender external? var fromInfo = this._hosts[fromUuid]; if (!fromInfo) { log.log(log.WARN, 'Received bus packet from unknown host (ignoring): %s', fromUuid); return; } if (!isArray(packet.path) || !isArray(packet.event)) { log.log(log.WARN, 'Invalid packet from %s: %j', fromUuid, packet); return; } // Process the packet based on whether it originated internally or // externally. This 'sequenceHost' is either the internal network // originator, or the external uuid if external. var sequenceHost; if (fromInfo.external) { sequenceHost = this._handleExternalBusPacket(packet, fromInfo); } else { sequenceHost = this._handleInternalBusPacket(packet, fromUuid); } if (!sequenceHost) { return; } // Cleanup the sequence number packet.seq = parseInt(packet.seq); // Update the sequence information if (packet.seq <= sequenceHost.sequence) { log.log(log.TRACE, 'Ignored duplicate event packet: %j', packet); return; } sequenceHost.sequence = packet.seq; if (fromInfo.external) { // Fake the message as having come from us within our // little internal network. I will be the 'originator' within // the internal network. packet.seq = this._sequence++; } // I don't want to send this packet along if I've already seen it. // Check the 'path' value to see if it has the given sequenceHost in // it already (more than once, since I just appended it), or // if it has my id. if (this._hasAlreadySeenPacket(packet, sequenceHost)) { log.log(log.TRACE, 'Already seen that packet (path violation)'); return; } // Log log.log(log.TRACE, 'Read event packet: %j', packet); this._emitPacket(packet, source); this._forwardPacket(packet, fromInfo, null); }; /** * This function will attempt to find more than two instances of the originator host, * or at least one instance of myself. If it finds either, then it will return * true, otherwise false. * @param packet * @param originatorHost * @private */ Bus.prototype._hasAlreadySeenPacket = function(packet, originatorHost) { var count = 0; for (var i = 0; i < packet.path.length; i++) { if (packet.path[i] == originatorHost.address) { count += 1; } if (packet.path[i] == this._id || count >= 2) { return true; } } return false; }; /** * Received the packet from internal. Just ensure we know who it came from. * @param packet * @returns {*} * @private */ Bus.prototype._handleInternalBusPacket = function(packet, fromUuid) { if (packet.path.length === 0) { log.log(log.WARN, 'Empty path from %s: %j', fromUuid, packet); return false; } // Take the most recent host in the path vector. This was the host in our // group that decided to re-send the message to me (or it was the external // host that sent the message to me) var originatorUuid = packet.path[packet.path.length - 1]; // Do we know about the originator? If not, ignore // Is the immediate packet sender external? var originatorInfo = this._hosts[originatorUuid]; if (!originatorInfo) { log.log(log.WARN, 'Received bus packet from unknown originator (ignoring): %s', originatorUuid); return false; } return originatorInfo; }; /** * Received the message from an external host. Set the src and mode, * and update the path to include the external link id. * @param packet * @param fromInfo * @private */ Bus.prototype._handleExternalBusPacket = function(packet, fromInfo) { // Append the sending host packet.path = packet.path.concat(fromInfo.address); // If global, then we've gotten the message through an // external link. We don't want to send it through any more external // links, so change it to 'group' mode. if (packet.mode === constants.Neighborhood.GLOBAL) { packet.mode = constants.Neighborhood.GROUP; } return fromInfo; }; /** * Handles EventEmitter messages * @param packet - the packet containing the event to emit * @param source - the source of the packet, @see{constants.Neighborhood} * @private */ Bus.prototype._emitPacket = function(packet, source) { try { if (EventEmitter.listenerCount(this, packet.event[0])) { var deliveryAddress = address(packet.path.concat(this._id)); var eventCopy = packet.event.slice(0); eventCopy.splice(1, 0, deliveryAddress, source); EventEmitter.prototype.emit.apply(this, eventCopy); } } catch (e) { log.log(log.ERROR, 'Exception executing event: %s %s', e.message, e.stack); } }; /** * * @param envelope * @param fromExternal * @private */ Bus.prototype._forwardPacket = function(envelope, fromInfo, destinationBridgeId, destinationHostId) { // Only send if we're in something greater than local mode! if (envelope.mode <= constants.Neighborhood.LOCAL) { return; } var fromSelf = envelope.path.length === 0; var fromExternal = fromInfo ? fromInfo.external : false; var fromUuid = fromInfo ? fromInfo.address : 'local'; // Special envelope that has myself appended to the path. var envelopeWithPathSequence; var envelopeWithPath; // Do not re-send the message to any host in the path vector // Using a hash here even though the array size is small because // indexOf isn't supported in IE8 var ignoreHosts = {}; for (var j = 0; j < envelope.path.length; j++) { ignoreHosts[envelope.path[j]] = true; } ignoreHosts[fromUuid] = true; log.log(log.TRACE, 'Relay event packet to hosts: %j', envelope); // Lookup the bridge var bridge; if (destinationBridgeId) { bridge = this._linkAssociation.getBridge(destinationBridgeId); } for (var host in this._hosts) { if (!ignoreHosts || !ignoreHosts[host]) { var hostInfo = this._hosts[host]; // If this is a bridge destination, then get the link id and ensure it's in // the bridge. if (bridge) { if (!bridge.hasLinkId(hostInfo.linkId)) { continue; } } // If the user has specified a host, then only emit to that host. if (destinationHostId) { if (host !== destinationHostId) { continue; } } // Only send to adjacent hosts if (hostInfo.adjacent) { // Only send if we're sending internal or we're sending to external // and we have at least Neighborhood.GLOBAL if (!hostInfo.external || (hostInfo.external && envelope.mode > constants.Neighborhood.GROUP)) { // Append my id to the path in special circumstances (outlined below). // When sending external, we don't append, because the external host will // append me in the appropriate circumstances. var envelopeToSend = envelope; if ((fromExternal && !hostInfo.external) || // from external to internal (fromSelf && !hostInfo.external) || // from self to internal (!fromSelf && !fromExternal && hostInfo.external)) { // from internal to external // append path if (!envelopeWithPath) { envelopeWithPath = xtend(envelope, { path: envelope.path.concat(this._id) }); } envelopeToSend = envelopeWithPath; } // If we're sending external, and it's not from ourself, then // we need to increment the sequence number. if (!fromSelf && hostInfo.external) { // append path if (!envelopeWithPathSequence) { envelopeWithPathSequence = xtend(envelope, { path: envelope.path.concat(this._id), seq: this._sequence++ }); } envelopeToSend = envelopeWithPathSequence; } this._routerInstance.sendPacket(host, 'bus', envelopeToSend, fromUuid); } } } } }; /** * Create the packet to send to external hosts * @param neighborhood * @param arguments * @private */ Bus.prototype._createPacket = function(neighborhood, event) { var packet = { event: event, seq: this._sequence++, mode: neighborhood, path: [] }; return packet; }; /** * Send to given host and neighborhood only * @param destinationBridgeId - only send to all links in this bridge * @param destinationHostId - only send to this host (as long as it's in the destination bridge) * @param neighborhood */ Bus.prototype.emitDirect = function(destinationBridgeId, destinationHostId, neighborhood) { var packet = this._createPacket(neighborhood, this._convertArgs(arguments, 3)); this._forwardPacket(packet, null, destinationBridgeId, destinationHostId); }; /** * Send to given neighborhood * @param neighborhood */ Bus.prototype.emit = function(neighborhood) { var packet = this._createPacket(neighborhood, this._convertArgs(arguments, 1)); this._emitPacket(packet, constants.Neighborhood.LOCAL); this._forwardPacket(packet, null, null, null); }; /** * Convert the given arg array into an array * @param args * @returns {Array} * @private */ Bus.prototype._convertArgs = function(args, start) { var event = []; for (var i = start; i < args.length; i++) { event.push(args[i]); } return event; };