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

491 lines (433 loc) 14.5 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 Endpoint = require('../../endpoint/endpoint'), inherits = require('util').inherits, format = require('util').format, uuid = require('node-uuid'), callContext = require('./context'), address = require('../../routing/address'), constants = require('../../util/constants'), appUtils = require('../../util/appUtils'), log = appUtils.getLogger(__filename); inherits(ObjectInstance, Endpoint); module.exports = ObjectInstance; /** * This represents an instance of a remote facade for one of my published * adapters. This client instance is an event emitter which is emitted * to an instance of an Endpoint.js adapter when someone tries to use it. * @augments Endpoint * @param {EndpointManager} endpointManager - used to track the endpoint * @param {Object} settings * @param {String} settings.name - the name of this object instance, derived from adapter name * @param {String} settings.clientInstance - the client instance this object instance belongs to * @param {String} settings.remoteId - the id of the remote facade endpoint * @param {Object} settings.object - the object this instance represents * @constructor */ function ObjectInstance(endpointManager, settings) { if (!(this instanceof ObjectInstance)) { return new ObjectInstance(endpointManager, settings); } var adapter = settings.clientInstance.getAdapter(); // Call parent constructor ObjectInstance.super_.call(this, endpointManager, { type: constants.EndpointType.OBJECT_INSTANCE, id: uuid(), identification: format('[name: %s] [version: %s]', settings.name, adapter.getVersion()) } ); // Register the streamer & messenger to receive messages from externals this.registerDefaultStreamerListener(); this.registerDefaultMessengerListener(); // Pending call contexts this._name = settings.name; this._object = settings.object; this._clientInstance = settings.clientInstance; this._remoteId = settings.remoteId; this._contexts = {}; this._contextsCount = 0; // Object instance starts as connected this._remoteConnected = true; // Bootstrap the API for this object this._methodIndex = this._createMethodIndex(); log.log(log.DEBUG, 'Created %s', this); } /** * Returns the name of this object instance * @return {String} */ ObjectInstance.prototype.getName = function() { return this._name; }; /** * Return the object this instance is wrapping * @returns {*} */ ObjectInstance.prototype.getObject = function() { return this._object; }; /** * Returns the client instance assigned to this object */ ObjectInstance.prototype.getClientInstance = function() { return this._clientInstance; }; /** * Returns the remote address of the facade this client instance * is connected to * @returns {*} */ ObjectInstance.prototype.getRemoteAddress = function() { return this.getClientInstance().getRemoteAddress(); }; /** * Returns the remote id of the facade this client instance * is connected to * @returns {*} */ ObjectInstance.prototype.getRemoteId = function() { return this._remoteId; }; /** * Return or create a context. * @param callId * @param createIfNotFound * @returns {*} */ ObjectInstance.prototype.getContext = function(callId, createIfNotFound) { if (this.hasContext(callId)) { return this._contexts[callId]; } else if (createIfNotFound) { if (!appUtils.isUuid(callId)) { throw new Error('invalid context id'); } var context = this._contexts[callId] = callContext(this, callId); this._contextsCount += 1; if (this._contextsCount == 1) { // Ensure no stale contexts this.getEndpointManager().registerPeriodic(this); } return context; } return null; }; /** * Get an API response for this object instance * @return {Object} API response */ ObjectInstance.prototype.getApi = function() { return { id: this.getId(), methods: this.getMethodNames() }; }; /** * Return a list of method names registered with this adapter. */ ObjectInstance.prototype.getMethodNames = function() { return Object.keys(this._methodIndex); }; /** * * @param callId * @returns {boolean} */ ObjectInstance.prototype.hasContext = function(callId) { if (this._contexts[callId]) { return true; } return false; }; /** * Handle a stream creation event from a remote facade or client instance * @param fromUuid * @param stream */ ObjectInstance.prototype._handleStream = function(stream, opts) { var type = stream.meta.type; var callId = stream.meta.id; // If the affinity is lost, end the stream. this.attachStream(stream); if (callId) { var context; switch (type) { case 'input': context = this.getContext(callId, true); context.setInputStream(stream); context.setBuffered(!opts.objectMode); break; case 'output': context = this.getContext(callId, true); context.setOutputStream(stream); break; default: log.log(log.ERROR, 'Malformed stream: %j for %s', stream.meta, this); stream.end(); return; } // Tell the call originator that the remote stream is ready. this.getMessenger().sendMessage(this.getRemoteAddress(), callId, { type: 'stream-connected' }); } }; /** * Handle an API request from a remote facade. * @param message */ ObjectInstance.prototype._handleMessage = function(message, source) { // Ensure that the source is within the expected neighborhood if (source > this._neighborhood) { return; } var callId = message.id; var callType = message.type; if (callId && callType) { switch (callType) { case 'close': this._remoteConnected = false; this.close(); return; case 'remote-stream': this._establishRemoteStream(callId, message); return; case 'call-facade': case 'call-ignore': case 'call': this._callMethod(callId, callType, message); return; case 'cancel': this.cancel(callId); return; } } log.log(log.ERROR, 'Malformed message: %j for %s', message, this); }; /** * Call the given method, executing the callback when finished * @param callId * @param callType * @param message * @private */ ObjectInstance.prototype._callMethod = function(callId, callType, message) { // Execute the context/call var context = this.getContext(callId, true); // Convert arguments, looking for Facades if (message.xargs && message.xargs.length > 0) { for (var i = 0; i < message.xargs.length; i++) { var arg = message.xargs[i]; var id = message.args[arg]; var remote = this.getClientInstance().getObjectInstance(id); if (remote) { message.args[arg] = remote.getObject(); } else { log.log(log.WARN, 'Unknown object id: %s', id); } } } // This method will process the result & send the // result message to the facade var resultFunction = function(type, data) { // Remove the context since it's finished this.removeContext(callId); if (type == 'result') { result = { type: 'result' }; if (callType == 'call') { // Only return the result if requested result.value = data; } else if (callType == 'call-facade') { // Derive the new name for the object instance; var newName = format('%s.%s', this.getName(), message.func); // Register the result as a facade. var objectInstance = this.getClientInstance() .createObjectInstance(newName, data, message.facadeId, this); if (objectInstance) { result.value = objectInstance.getApi(); } else { result = { type: 'error', message: 'Could not create the object instance', name: 'Error' }; } } } else { result = { type: 'error', message: data.message, name: data.name }; } // Send the result this.getMessenger().sendMessage( this.getRemoteAddress(), callId, result); }.bind(this); // Make sure the function exists, and call it. var result; if (this.hasMethod(message.func)) { context.execute(message.func, message.args, resultFunction); } else { log.log(log.ERROR, 'Method does not exist: %s for %s', message.func, this); resultFunction('error', new Error('Method not found')); } }; /** * Create a stream to the remote client instance from a facade request. * @param callId * @param message * @private */ ObjectInstance.prototype._establishRemoteStream = function(callId, message) { var context = this.getContext(callId, true); // Parse the remote address from the metadata var desiredRemoteAddress = message.remoteAddress, desiredRemoteId = message.remoteId; // Create a route to the destination var streamAddress = this.getRemoteAddress().routeThrough(address(desiredRemoteAddress, true)); // Create the remote stream. var stream = this.getStreamer().createStream( desiredRemoteId, streamAddress, { id: message.callId, type: 'input' }, { objectMode: !message.buffered }); // If the affinity is lost, end the stream. this.attachStream(stream); this.getMessenger().sendMessage(this.getRemoteAddress(), callId, { type: 'stream-connected' }); // Connect it to the call context.setOutputStream(stream); }; /** * Whether the method is registered in the method index * @param name */ ObjectInstance.prototype.hasMethod = function(name) { return !!this._methodIndex[name]; }; /** * Create a list of method names registered with this adapter. */ ObjectInstance.prototype._createMethodIndex = function() { var methodIndex = {}; var obj = this._object; // Generate the methods. var total = 0; for (var prop in obj) { if (typeof (obj[prop]) == 'function' && prop.charAt(0) !== '_') { methodIndex[prop] = true; total += 1; } } log.log(log.DEBUG2, 'Counted %s functions in object for %s', total, this); return methodIndex; }; /** * Ensure no contexts are stale * @private */ ObjectInstance.prototype.performPeriodic = function() { var contexts = Object.keys(this._contexts); for (var i = 0; i < contexts.length; i++) { var callId = contexts[i]; var ctx = this._contexts[callId]; ctx.incrementPeriodic(); if (ctx.getPeriodic() > 2) { log.log(log.DEBUG, 'Context is stale [ctx: %s] for %s', callId, this); this.cancel(callId); } } }; /** * Cancel the given call and clean it up * @param callId */ ObjectInstance.prototype.cancel = function(callId) { if (this.hasContext(callId)) { this._contexts[callId].cancel(); this.removeContext(callId); } }; /** * Stop listening for periodic updates & remove the context * @param callId */ ObjectInstance.prototype.removeContext = function(callId) { if (this.hasContext(callId)) { delete this._contexts[callId]; this._contextsCount -= 1; if (this._contextsCount === 0) { this.getEndpointManager().unregisterPeriodic(this); } } }; /** * Cancel all contexts * @private */ ObjectInstance.prototype._handleClose = function(affinityClosure) { // Tell the remote we're closing! if (this._remoteConnected && !affinityClosure) { this.getMessenger().sendMessage( this.getRemoteAddress(), this._remoteId, { id: this.getId(), type: 'close' }); } // Close all contexts var contexts = Object.keys(this._contexts); for (var callId in contexts) { this.cancel(callId); } };