UNPKG

happner

Version:

distributed application engine with evented storage and mesh services

503 lines (357 loc) 16 kB
;(function(isBrowser) { var Logger, Promisify, MeshError; if (isBrowser) { window.Happner = window.Happner || {}; window.Happner.Messenger = Messenger; Promisify = Happner.Promisify; MeshError = Happner.MeshError; } else { module.exports = Messenger; Promisify = require('./promisify'); Logger = require('happn-logger'); MeshError = require('./mesh-error'); } function Messenger(endpoint, mesh){ var exchange = mesh.exchange; if (typeof mesh.log == 'object' && mesh.log.createLogger) { this.log = mesh.log.createLogger('Messenger/' + endpoint.name); } else if (Logger) { if (!Logger.configured) Logger.configure(); this.log = Logger.createLogger('Messenger/' + endpoint.name); } else { this.log = Happner.createLogger('Messenger/' + endpoint.name); } this.log.$$TRACE('creating new messenger for endpoint \'%s\'', endpoint.name); Object.defineProperty(this, '_endpoint', {value: endpoint}); Object.defineProperty(this, '_exchange', {value: exchange}); // Object.defineProperty(this, '_meshDescription', {value: endpoint.description}); Object.defineProperty(this, '__responseHandlerCache', {value: {}}); if (typeof __serializer !== 'undefined') this.serializer = __serializer else if (typeof mesh.serializer !== 'undefined') this.serializer = mesh.serializer; this.requestors = {}; this.initialized = {}; this.responseHandlers = {}; this.eventRegister = 0; this.listening = false; this.dataconfig = mesh.config.datalayer; this.session = {"id":endpoint.data.session.id}; if (endpoint.data.session.user) this.session.username = endpoint.data.session.user.username; if (!this._endpoint.description.setOptions) this._endpoint.description.setOptions = {noStore:true} } Messenger.prototype.__ensureResponseHandler = function(responseAddress, callback){ if (!this.__responseHandlerCache[responseAddress]){ var _this = this; var endpoint = _this._endpoint; responseMask = '/_exchange/responses/'+responseAddress+'/*'; _this.log.$$TRACE('ensuring response handler - data.on( %s', responseMask); endpoint.data.on(responseMask, {event_type:'set', count:0}, _this.responseHandler.bind(_this), function(e){ if (e) { _this.log.$$TRACE('subscribe ERROR - data.on( %s', responseMask, e); }else{ _this.__responseHandlerCache[responseAddress] = true; _this.log.$$TRACE('subscribe OK - data.on( %s', responseMask); } callback(e); } ); } else callback(); } Messenger.prototype.updateRequestors = function(createComponents, destroyComponents, callback) { var endpoint = this._endpoint; var _this = this; this.createRequestors(createComponents) .then(function() { return _this.destroyRequestors(destroyComponents) }) .then(function() { if (_this.dataconfig.secure){ _this.listening = true; return callback(); } if (_this.listening) return callback(); // Set to listening for responses // ------------------------------ _this.listening = true; // set to true BEFORE listening success to avoid // race condition (multiple going to listen) // Wildcard listen on response path var responseMask = '/_exchange/responses' + '/' + _this.session.id + '/*'; _this.log.$$TRACE('unsecure subscribe START - data.on( %s', responseMask); endpoint.data.on(responseMask, {event_type:'set', count:0}, _this.responseHandler.bind(_this), function(e){ if (e) { // TODO: Consider configable retry! // This subscription is Vital. _this.listening = false; _this.log.$$TRACE('unsecure subscribe ERROR - data.on( %s', responseMask, e); return callback(e); } _this.log.$$TRACE('unsecure subscribe OK - data.on( %s', responseMask); return callback(); } ); }) .catch(callback); } Messenger.prototype.createRequestors = Promisify(function(componentNames, callback) { var endpoint = this._endpoint; var components = endpoint.description.components; var _this = this; componentNames.forEach(function(componentName) { var componentDescription = components[componentName]; Object.keys(componentDescription.methods).forEach(function(methodName) { var requestAddress = '/' + endpoint.name + '/' + componentName + '/' + methodName; var description = componentDescription.methods[methodName]; _this.log.$$DEBUG('creating requestor at %s', requestAddress); // The requestor() // --------------- var requestor = function() { var requestPath = '/_exchange/requests' + requestAddress; _this.__prepareMessage(endpoint.name, componentName, methodName, _this.session.id, description, arguments[0], function(e, message){ if (e) return _this.__discardMessage(message, e); if (_this.serializer && typeof _this.serializer.__encode == 'function') { message.args = _this.serializer.__encode( message.args, { req: true, res: false, src: { mesh: mesh.config.name, browser: isBrowser, //////// component: 'possible?' }, dst: { endpoint: endpoint.name, component: componentName, method: methodName, }, addr: message.callbackAddress, opts: endpoint.description.setOptions, }); } _this.log.$$TRACE('data.set( %s', requestPath); endpoint.data.set(requestPath, message, endpoint.description.setOptions, _this._createPubResponseHandle(message) ); }); } // Assign specific requestor for this requestAddress _this.requestors[requestAddress] = requestor; // Assign `this` as messenger on the exchange _this._exchange[requestAddress] = _this; }); // Mark component as initialized in this messenger _this.initialized[componentName] = Date.now(); }); callback(); // no particular need. }); Messenger.prototype.destroyRequestors = Promisify(function(componentNames, callback) { // TODO: Deal with inprogress requests, don't leave client hanging on callback... if (componentNames.length == 0) return callback(); var endpoint = this._endpoint; var components = endpoint.previousDescription.components; var _this = this; componentNames.forEach(function(componentName) { var componentDescription = components[componentName]; Object.keys(componentDescription.methods).forEach(function(methodName) { var requestAddress = '/' + endpoint.name + '/' + componentName + '/' + methodName; _this.log.$$DEBUG('destroying requestor at %s', requestAddress); delete _this.requestors[requestAddress]; delete _this._exchange[requestAddress]; }); delete _this.initialized[componentName]; // <-- had timestamp, could do component uptime }); callback(); }); Messenger.prototype.deliver = function(address){ // remove address and pass args to requestor as array this.log.$$TRACE('deliver( %s', address); var args = Array.prototype.slice.call(arguments); args.shift(); this.requestors[address](args); } Messenger.prototype._systemEvents = function(event, data) { this.log.warn(event, data); } Messenger.prototype.__discardMessage = function(){ this.log.$$TRACE('data.set( %j', arguments); } Messenger.prototype.__validateMessage = function(methodDescription, args, callback){ try{ if (!methodDescription) return callback(new MeshError('missing methodDescription')); // throw new MeshError('Component does not have the method: ' + method); //do some schema based validation here var schemaValidationFailures = []; methodDescription.parameters.map(function(parameterDefinition, index){ if (parameterDefinition.required && !args[index]) schemaValidationFailures.push({"parameterDefinition":parameterDefinition, "message":"Required parameter not found"}); }); if (schemaValidationFailures.length > 0) return callback(new MeshError('schema validation failed', e)); // NO! if the callback throws then a second callback will be made with the validation 'failed error' // // callback(); }catch(e){ return callback(new MeshError('validation failed', e)); } callback(); } Messenger.prototype.__callbackImmediate = function(args, e, result){ try{ if (args.length > 1){ //bluebird adds something to the end var callbackFunction = args[args.length - 1]; if (callbackFunction && typeof(callbackFunction) == 'function'){ callbackFunction(e, result); } } }catch(e){ this.log.error('failed running callback immediate', e); } } Messenger.prototype.__createMessage = function(callbackAddress, methodDescription, args, callback){ var _this = this; var message = {"callbackAddress":callbackAddress, args:[], origin:_this.session}; methodDescription.parameters.map(function(parameterDescription, index){ if (parameterDescription["type"] == 'callback' || ( typeof(args[index]) == 'function' && index == args.length - 1)){ // // If the argument is a function it is only interpreted as the // callback if it is the last argument. // // But functions as arguments cannot be sent across the network // so in most cases it is inappropriate to pass function arguments // across the exchange. // // Functions can however be sent as arguments across the intra-process // datalayer. // // So exchange calls from the server to itself can have function params. // // This ability is used in the system/data component's on() function // // if (!args[index]) throw new MeshError('Callback for ' + address + ' was not defined'); if (typeof (args[index]) != 'function') throw new MeshError('Invalid callback for ' + address + ', callback must be a function'); var callbackHandler = { "handler":args[index], "callbackAddress":message.callbackAddress }; callbackHandler.handleResponse = function(argumentsArray){ clearTimeout(this.timedout); delete _this.responseHandlers[this.callbackAddress]; try{ return this.handler.apply(this.handler, argumentsArray); }catch(e){ _this.log.error('error in response handler', e); return; } }.bind(callbackHandler); callbackHandler.timedout = setTimeout(function(){ delete _this.responseHandlers[this.callbackAddress]; return this.handler("Request timed out"); }.bind(callbackHandler), _this._endpoint.description.setOptions.timeout || 5000); _this.responseHandlers[message.callbackAddress] = callbackHandler; } else if (args.length >= index + 1) { // include only args that were in the call // rather that padding in undefined's accoding to the config's arg definitions message.args.push(args[index]); } }); return callback(null, message); } Messenger.prototype.__prepareMessage = function(endpointName, componentName, methodName, clientId, methodDescription, args, callback){ var _this = this; _this.__validateMessage(methodDescription, args, function(e){ if (e) return callback(e); var responseHandlerAddress = [endpointName, componentName, methodName, clientId].join('/'); var callbackAddress = '/_exchange/responses/' + responseHandlerAddress + '/' + _this.eventRegister++; if (_this.dataconfig.secure){ _this.__ensureResponseHandler(responseHandlerAddress, function(e){ if (e){ return _this.__callbackImmediate(args, e); // try and run the actual response method directly //return callback(e);//dont do anything more } _this.__createMessage(callbackAddress, methodDescription, args, callback); }); }else{ callbackAddress = '/_exchange/responses/' + [clientId, endpointName, componentName, methodName].join('/') + '/' + _this.eventRegister++; _this.__createMessage(callbackAddress, methodDescription, args, callback); } }); } Messenger.prototype.responseHandler = function(response, meta) { if (response.status == 'error' && !meta){ return this._systemEvents('nohandler', response); } this.log.$$TRACE('responseHandler( %s', meta.path); if (this.serializer && typeof this.serializer.__decode == 'function') { response.args = this.serializer.__decode(response.args, { req: false, res: true, meta: meta, }); } var responseHandler = this.responseHandlers[meta.path]; if (responseHandler){ if (response.status == 'ok'){ responseHandler.handleResponse(response.args); } else{ var error; var serializedError = response.args[0]; if (error instanceof Error) { error = serializedError; } else { error = new Error(serializedError.message); error.name = serializedError.name; delete serializedError.message; delete serializedError.name; Object.keys(serializedError).forEach(function(key){ error[key] = serializedError[key]; }); } response.args[0] = error; responseHandler.handleResponse(response.args); } } else { this._systemEvents('nohandler', response); } } Messenger.prototype._createPubResponseHandle = function(message) { var _this = this; return function(e, response) { if (e) { var assembledFailure = { status: 'error', args: [ e instanceof Error ? e : { message: e.toString(), name: e.name } ] } return _this.responseHandler(assembledFailure, {path:message.callbackAddress}); } _this.log.$$TRACE('request successful'); } } // Messenger.prototype.__createSubResponseHandle = function(responseAddress) { // return this.responseHandler.bind(this); // } })(typeof module !== 'undefined' && typeof module.exports !== 'undefined' ? false : true);