UNPKG

happner

Version:

distributed application engine with evented storage and mesh services

1,732 lines (1,333 loc) 71 kB
/** * Created by Johan on 4/14/2015. */ // process.env.NO_EXIT_ON_UNCAUGHT (not recommended // - unless your boot-time is v/long and your confidence is v/high // - it logs error, beware log spew // TODO: log repeated message "collapse" (by context, by level?) // ) // process.env.NO_EXIT_ON_SIGTERM // process.env.STOP_ON_SIGINT (just stops components and disconnects network, no exit) <--- functionality pending // process.env.START_AS_ROOTED (exposes all mesh nodes in process at global.$happner) // process.env.UNROOT_ON_REPL (removes $happner from global and attaches it to repl server context during startup) // process.env.SKIP_AUTO_LOAD (don't perform auto loading from happner.js) var root = { nodes: {}, utils: {} } var depWarned0 = false; // mesh.api var depWarned1 = false; // componentConfig.meshName var depWarned2 = false; // componentConfig.setOptions var depWarned3 = false; // module.directory var depWarned4 = false; // Happner.start() if (process.env.START_AS_ROOTED) global.$happner = root; var Internals = require('./system/shared/internals') , MeshClient = require('./system/shared/mesh-client') , Happn = require('happn') , DataLayer = require('./system/datalayer') , Config = require('./system/config') , async = require('async') , MeshError = require('./system/shared/mesh-error') , ComponentInstance = require('./system/component-instance') , path = require("path") , repl = require('./system/repl') , fs = require('fs-extra') , Promise = require('bluebird') , Packager = require('./system/packager') , utilities = require('./system/utilities') , Logger = require('happn-logger') , events = require("events") , EventEmitter = require("events").EventEmitter ; module.exports = Mesh; module.exports.MeshClient = MeshClient; var _meshEvents = new EventEmitter(); var __emit = function (key, data) { _meshEvents.emit(key, data); } module.exports.on = function (key, handler) { return _meshEvents.on(key, handler); } module.exports.off = function (key, handler) { return _meshEvents.removeListener(key, handler); } Logger.emitter.on('after', function (level, message, stack) { __emit('mesh-log', {"level": level, "message": message, "stack": stack}); }.bind(this)); // Quick start. module.exports.create = Promise.promisify(function MeshFactory(config, callback) { // node -e 'require("happner").create()' config = config || {}; callback = callback || (typeof config == 'function' ? config : function (err) { if (err) { console.error(err.stack); process.exit(err.errno || 1); } }); // node -e 'require("happner").create(9999)' if (typeof config == 'number') config = { datalayer: { port: config } }; // node -e 'require("happner").create("5.6.7.88:7654")' if (typeof config == 'string') { var parts = config.split(':'); config = { datalayer: {} }; config.datalayer.host = parts[0]; if (parts[1]) config.datalayer.port = parseInt(parts[1]); } ; (new Mesh()).initialize(config, function (err, mesh) { if (err) return callback(err, mesh); return mesh.start(function (err, mesh) { if (err) return callback(err, mesh); callback(null, mesh); }); }); }); Object.defineProperty(module.exports, 'start', { get: function () { if (!depWarned4) { depWarned4 = true; console.warn('\n[ WARN] Happner.start() is deprecated, use Happner.create().\n'); } return module.exports.create; } }); var __protectedMesh = function(config, meshContext){ return { // (all false) - runlevel 0 initializing: false, // true - runlevel 10 initialized: false, // true - runlevel 20 starting: false, // true - runlevel 30 started: false, // true - runlevel 40 stopped: false, // true - runlevel 00 config: config || {}, elements: {}, description: {}, endpoints: {}, exchange: {}, datalayer: {}, // Selected internal functions available to _mesh. _createElement: function (spec, writeSchema, callback) { meshContext._createElement(spec, writeSchema, callback); }, _destroyElement: function (componentName, callback) { meshContext._destroyElement(componentName, callback); } // TODO: add access to start() and stop() }; } function Mesh(config) { var _this = this; // _mesh // ----- // // if meshConfig: { components: { 'componentName': { accessLevel: 'mesh', ... // then $happn._mesh is available in component this._mesh = __protectedMesh(config, _this); this._stats = { proc: { up: Date.now() }, component: {} }; // used by packager.js & modules/api Object.defineProperty(this, 'tools', { value: {} }); Object.defineProperty(this, 'runlevel', { get: function () { if (_this._mesh.stopped) return 0; if (_this._mesh.started) return 40; if (_this._mesh.starting) return 30; if (_this._mesh.initialized) return 20; if (_this._mesh.initializing) return 10; return 0; }, // set: function(n) { // // }, enumerable: true }); this.initialize = this.initialize; // make function visible in repl (console) this.start = this.start; this.stop = this.stop; this.describe = this.describe; this.test = this.test; Object.defineProperty(this, 'api', { get: function () { if (depWarned0) return _this; _this.log.warn('Use of mesh.api.* is deprecated. Use mesh.*'); try { _this.log.warn(' - at %s', _this.getCallerTo('mesh.js')); } catch (e) { } depWarned0 = true; return _this; } }); } // Step1 of two step start with mandatory args (initialize(function Callback(){ start() })) Mesh.prototype.__initialize = function (config, callback) { // TODO: this function is +- 250 lines... (hmmmm) var _this = this; //so we can redefine properties without failure if (_this._mesh.stopped) { _this.log.warn('reinitializing previous mesh'); Internals = require('./system/shared/internals'); _this._mesh = __protectedMesh(config, _this); } _this.__events = new EventEmitter(); _this.on = function (key, handler) { return this.__events.on(key, handler); }.bind(_this); _this.removeListener = function (key, handler) { return this.__events.removeListener(key, handler); }.bind(_this); _this.emit = function (key, value) { return this.__events.emit(key, value); }.bind(_this); _this.startupProgress = function (log, progress) { if (_this.log && _this.log.info) _this.log.info('startup progress: ' + log, progress); __emit('startup-progress', {"log": log, "progress": progress}); }; _this._mesh.initialized = false; _this._mesh.initializing = true; //util.inherits(this._mesh, EventEmitter); _this._mesh.caller = _this.getCallerTo('mesh.js'); if (typeof config == 'function') { callback = config; config = _this._mesh.config; // assume config came on constructor } else if (!config) { config = _this._mesh.config; // again } else if (config) { _this._mesh.config = config; } else { config = _this._mesh.config; } Object.defineProperty(_this, 'version', { value: require(__dirname + '/../package.json').version, enumerable: true }); // First mesh in process configures logger if (!Logger.configured) Logger.configure(config.util); _this.log = Logger.createLogger('Mesh'); _this.log.context = config.name; _this.startupProgress("initializing data layer...", 56); //we first need the datalayer up, with a name _this._initializeDataLayer(config, function (e) { if (e) return callback(e); _this.startupProgress("initialized data layer", 63); var configer = new Config(); // Async config step for (ask what i should do)ness <--- functionality pending configer.process(_this, config, function (e, config) { if (e) return callback(e); _this.startupProgress("configured mesh", 64); root.nodes[config.name] = _this; _this.log.createLogger('Mesh', _this.log); _this.log.$$DEBUG('initialize'); if (!config.home) { config.home = config.home || _this._mesh.caller.file ? path.dirname(_this._mesh.caller.file) : null } if (!config.home) { _this.log.warn('home unknown, require() might struggle'); } else { _this.log.info('home %s', config.home); } // Need to add the caller's module path as first for module searches. _this.updateSearchPath(config.home); _this.log.info('happner v%s', _this.version); _this.log.info('config v%s', (config.version || '..')); _this.log.info('localnode \'%s\' at pid %d', config.name, process.pid); Object.defineProperty(_this._mesh, 'log', { value: _this.log }); if (process.env.START_AS_ROOTED && !config.repl) { config.repl = { socket: '/tmp/socket.' + config.name } } _this.exchange = {}; _this.event = {}; //_this.data = _this._mesh.data; // Need to filter access to the data object // eg. $happn.data.pubsub.happn.server.close(); // (stops the http server in happn, taking down the whole system) // removed for performance cost, we need to hide things in HAPPN /* [ 'on', 'off', 'get', 'getPaths', 'set', 'setSibling', 'session', 'remove', ].forEach(function(name) { Object.defineProperty(_this.data, name, { value: function() { if (typeof _this._mesh.data[name] == 'function') { return _this._mesh.data[name].apply(_this._mesh.data, arguments); } return _this._mesh.data[name]; }, enumerable: name == 'session' ? false : true }) }); */ repl.create(_this); _this.startupProgress("created repl", 65); var stopmesh = function (mesh, last) { if (!mesh._mesh.initialized) process.exit(0); mesh.stop({exitCode: 0, kill: last}, function (e) { mesh._mesh.initialized = false; if (e) mesh.log.warn('error during stop', e); }); } var pausemesh = function (mesh) { mesh.stop(function (e) { mesh._mesh.initialized = false; if (e) mesh.log.warn('error during stop', e) }); } _this.__onUncaughtException = function (err) { if (process.env.NO_EXIT_ON_UNCAUGHT) { _this.log.error('UNCAUGHT EXCEPTION', err); return; } _this.log.fatal('uncaughtException (or set NO_EXIT_ON_UNCAUGHT)', err); process.exit(1); }; _this.__onExit = function (code) { Object.keys(root.nodes).forEach( function (name) { root.nodes[name].stop(); root.nodes[name].log.info('exit %s', code); } ) }; _this.onSIGTERM = function (code) { console.log(); if (process.env.NO_EXIT_ON_SIGTERM) { _this.log.warn('SIGTERM ignored'); return; } var last = 0; Object.keys(root.nodes).forEach( function (name) { last++; root.nodes[name].log.warn('SIGTERM'); stopmesh(root.nodes[name], last == Object.keys(root.nodes).length); } ); }; _this.onSIGINT = function () { console.log(); if (process.env.STOP_ON_SIGINT) { Object.keys(root.nodes).forEach( function (name) { root.nodes[name].log.warn('SIGINT without exit'); pausemesh(root.nodes[name]); } ); return; } var last = 0; Object.keys(root.nodes).forEach( function (name) { last++; root.nodes[name].log.warn('SIGINT'); stopmesh(root.nodes[name], last == Object.keys(root.nodes).length); } ); }; _this.unsubscribeFromProcessEvents = function(logStopEvent){ try{ process.removeListener('uncaughtException', _this.__onUncaughtException); process.removeListener('exit', _this.__onExit); process.removeListener('SIGTERM', _this.onSIGTERM); process.removeListener('SIGINT', _this.onSIGINT); logStopEvent('$$DEBUG', 'unsubscribed from process events'); }catch(e){ logStopEvent('error', 'failed to unsubscribe from process events', e); //do nothing } } if (Object.keys(root.nodes).length == 1) { // only one listener for each below // no matter how many mesh nodes in process process.on('uncaughtException', _this.__onUncaughtException); process.on('exit', _this.__onExit); process.on('SIGTERM', _this.onSIGTERM); process.on('SIGINT', _this.onSIGINT); // TODO: capacity to reload config! (?tricky?) // process.on('SIGHUP', function() { // _this.log.info('SIGHUP ignored'); // }); } if (!config.modules) config.modules = {}; if (!config.components) config.components = {}; // autoload can be set to false (to disable) or alternative configname to load if (typeof config.autoload == 'undefined') config.autoload = 'autoload'; if (typeof config.autoload == 'boolean') { if (config.autoload) { config.autoload = 'autoload'; } } _this.attachSystemComponents(config); _this.startupProgress("attached system components", 66); async = require("async"); async.series([ function (callback) { if (!config.autoload || process.env.SKIP_AUTO_LOAD) { return callback(); } _this.getPackagedModules(config, callback); }, function (callback) { _this.startupProgress("got packaged modules", 67); if (!config.autoload || process.env.SKIP_AUTO_LOAD) { return callback(); } _this.log.$$DEBUG('searched for autoload'); _this.loadPackagedModules(config, null, callback); }, function (callback) { _this.startupProgress("got autoload modules", 68); if (!config.autoload || !process.env.SKIP_AUTO_LOAD) { _this.log.$$DEBUG('initialized autoload'); } _this._loadComponentSuites(config, callback); }, function (callback) { _this.startupProgress("loaded component suites", 69); _this.log.$$DEBUG('loaded component suites'); _this._registerInitialSchema(config, callback); }, function (callback) { _this.startupProgress("registered initial schema", 70); _this.log.$$DEBUG('registered initial schema'); _this._initializePackager(callback); }, function (callback) { _this.log.$$DEBUG('initialized packager'); _this.startupProgress("initialized packager", 78); var isServer = true; var describe = {}; Internals._initializeLocal(_this, describe, config, isServer, callback); }, function (callback) { _this.startupProgress("initialized local", 79); _this.log.$$DEBUG('initialized local'); _this._initializeElements(config, callback); }, function (callback) { _this.startupProgress("initialized elements", 90); _this.log.$$DEBUG('initialized elements'); _this._registerSchema(config, true, callback); }, function (callback) { _this.startupProgress("registered schema", 91); _this.log.$$DEBUG('registered schema'); _this._initializeEndpoints(callback); }, function (callback) { _this.log.$$DEBUG('initialized endpoints'); // TODO: This point is never reached if any of the endpoints are hung before writing their description // (https://github.com/happner/happner/issues/21) // Internals._attachProxyPipeline(_this, _this.describe(), Happn, config, callback); // }, // function(callback) { // _this.log.$$DEBUG('attached to proxy pipeline'); callback(); } ], function (e) { if (!e) _this.log.info('initialized!'); _this._mesh.initialized = true; _this._mesh.initializing = false; callback(e, _this); }); }); }); }; Mesh.prototype.initialize = Promise.promisify(function (config, callback) { var _this = this; if (config.deferListen) { if (!config.datalayer) config.datalayer = {}; config.datalayer.deferListen = true; } _this.__initialize(config, callback); }); // Step2 of two step start (initialize({},function callback(){ start() })) Mesh.prototype.start = Promise.promisify(function (callback) { var _this = this; if (!_this._mesh.initialized) return console.warn('missing initialize()'); _this._mesh.starting = true; _this._mesh.started = false; _this.startupProgress("starting mesh", 92); var waiting = setInterval(function () { Object.keys(_this._mesh.calls.starting).forEach(function (name) { _this.log.warn('awaiting startMethod \'%s\'', name); }); }, 10 * 1000); var impatient = setTimeout(function () { Object.keys(_this._mesh.calls.starting).forEach(function (name) { _this.log.fatal('startMethod \'%s\' did not respond', name, new Error('timeout')); }); _this.stop({ kill: true, wait: 200 }) }, _this._mesh.config.startTimeout || 60 * 1000); _this.__startComponents(function (error) { clearInterval(waiting); clearTimeout(impatient); if (error) return callback(error, _this); _this._mesh.starting = false; _this._mesh.started = true; _this.log.info('started!'); _this.startupProgress("mesh started", 100); // if we need to listen with deferListen and no loader, now is the time if (_this._mesh.config.deferListen && !_this._mesh.config["happner-loader"]) { _this._mesh.datalayer.listen(function(err){ callback(err, _this); }); return; } callback(null, _this); }); }); Mesh.prototype.listen = Promise.promisify(function (options, callback) { var _this = this; if (typeof options == 'function') { callback = options; options = {}; } if (!options.times) options.times = 10; if (!options.interval) options.interval = 1000; if (!_this._mesh.config.deferListen) return callback(new Error('attempt to listen when deferListen is not enabled')); async.retry(options, _this._mesh.datalayer.listen, function (e) { if (e) return callback(e); callback(null, _this); }); }); Mesh.prototype.stop = Promise.promisify(function (options, callback) { var _this = this; if (!_this._mesh.initialized) return; _this._mesh.initialized = false; _this._mesh.initializing = false; _this._mesh.starting = false; _this._mesh.started = false; // TODO: more thought/planning on runlevels // this stop sets to 0 irrespective of success var stopEventLog = []; var logStopEvent = function(type, message, data){ if (!data) data = null; stopEventLog.push({"type":type, "message":message, "data":data}); _this.log[type](message); }; logStopEvent('$$DEBUG', 'initiating stop'); if (typeof options === 'function') { callback = options; options = {}; } if (options.kill && !options.wait) options.wait = 10000; var timeout; var kill = function () { process.exit((typeof options.exitCode == 'number') ? options.exitCode : 1); } if (options.kill) { timeout = setTimeout(function () { _this.log.error("failed to stop components, force true"); kill(); }, options.wait); } logStopEvent('$$DEBUG', 'stopping components'); this.__stopComponents(function (e) { // // TODO: Only one error! // Multiple components may have failed to stop. if (e) { // component instance already logged the err _this.log.error("failure to stop components"); /*if (timeout) { // Kill is pending. // Stop network even tho some components // have failed to stop // Dont wait for callback. console.log(_this._mesh.datalayer); _this._mesh.datalayer.server.stop(options, function(e) { if (e) return _this.log.error('datalayer stop error', e); _this.log.info('datalayer stopped'); }); _this.log.warn('datalayer not pending'); // Give the caller to stop the error, // they still some time to do something with it. if (callback) return callback(e, _this); return; }*/ // Kill is not pending. // Some components failed to stop. // Dont stop the network. // TODO: enable a second call to stop() // that does not stop components that // are already stopped. /* _this.log.warn('datalayer not stopped'); if (callback) return callback(e, _this); return;*/ } options.reconnect; // if present, it's being passed into server.stop() below. // causes primus to inform remotes to enter reconect loop // All components stopped ok logStopEvent('$$DEBUG', 'stopped components'); logStopEvent('$$DEBUG', 'stopping datalayer'); _this._mesh.datalayer.server.stop(options, function (e) { // Stop the pending kill (if present) // clearTimeout(timeout); if (e) { logStopEvent('error', 'datalayer stop error', e); if (options.kill) return kill(); return; } logStopEvent('$$DEBUG', 'stopped datalayer'); if (options.kill) kill(); delete root.nodes[_this._mesh.config.name]; //only do this when the stop is clean, we remove this mesh from the meshes collection - if the collection is empty // we unsubscribe from the process level events if (Object.keys(root.nodes).length == 0) _this.unsubscribeFromProcessEvents(logStopEvent); logStopEvent('info', 'stopped!'); _this._mesh.stopped = true;//this state allows for graceful reinitialization if (callback) callback(e, _this, stopEventLog); }); }); }); Mesh.prototype.describe = function (cached, componentCached) { if (typeof componentCached == 'undefined') componentCached = false; if (!this._mesh.config || !this._mesh.config.datalayer) throw new Error('Not ready'); if (this._mesh.description && cached == true) return this._mesh.description; // NB: destroyElement creates endpoint.previousDescription, // which relies on this description being built entirely anew. var description = { name: this._mesh.config.name, // initializing: this._mesh.initializing, // Still true at finel description write. // Needs true for remote endpoints to stop // waiting in their _initializeEndpoints initializing: false, components: {}, setOptions: this._mesh.config.datalayer.setOptions }; for (var componentName in this._mesh.elements) { if (this._mesh.elements[componentName].deleted) continue; description.components[componentName] = this._mesh.elements[componentName].component.instance.describe(componentCached); } this._mesh.endpoints[this._mesh.config.name].description = description; return this._mesh.description = description; }; Mesh.prototype.getCallerTo = function (skip) { var stack, file, parts, name, result = {}; var origPrep = Error.prepareStackTrace; Error.prepareStackTrace = function (e, stack) { return stack; } try { stack = Error.apply(this, arguments).stack; stack.shift(); stack.shift(); for (var i = 0; i < stack.length; i++) { file = stack[i].getFileName(); line = stack[i].getLineNumber(); colm = stack[i].getColumnNumber(); // Since using bluebird some .getFileName()'s are coming up undefined if (file) { parts = file.split(path.sep); // skip calls from the promise implementation if (parts.indexOf('bluebird') !== -1) continue; if (!skip) { result = { file: file, line: line, colm: colm }; break; } name = parts.pop(); if (name !== skip) { result = { file: file, line: line, colm: colm }; break; } } } } finally { Error.prepareStackTrace = origPrep; result.toString = function () { return result.file + ':' + result.line + ':' + result.colm }; return result; } }; Mesh.prototype.updateSearchPath = function (startAt) { var newPaths = []; if (!startAt) return; this.log.$$TRACE('updateSearchPath( before ', module.paths); var addPath = function (dir) { var add = path.normalize(dir + path.sep + 'node_modules'); if (module.paths.indexOf(add) >= 0) return; newPaths.push(add); } var apply = function (paths) { paths.reverse().forEach(function (path) { module.paths.unshift(path); }); } var recurse = function (dir) { addPath(dir); var next = path.dirname(dir); if (next.length < 2 || (next.length < 4 && next.indexOf(':\\') != -1 )) { addPath(next); return apply(newPaths); } recurse(next); } recurse(startAt); this.log.$$TRACE('updateSearchPath( after ', module.paths); }; Mesh.prototype._initializeDataLayer = function (config, callback) { var _this = this; _this._mesh.datalayer = DataLayer.create(_this, config, function (err, client) { if (err) return callback(err, _this); _this._mesh.data = client; callback(null, _this); } ); }; Mesh.prototype._initializePackager = function (callback) { Packager = require('./system/packager'); var packager = new Packager(this); packager.initialize(callback); }; Mesh.prototype._initializeElements = function (config, callback) { var _this = this; return async.parallel( Object.keys(config.components).map(function (componentName) { return function (callback) { var componentConfig = config.components[componentName]; var moduleName = componentConfig.moduleName || componentConfig.module || componentName; componentConfig.moduleName = moduleName; // pending cleanup of .moduleName usage (preferring just 'module') var moduleConfig = config.modules[moduleName]; if (!moduleConfig || moduleName[0] == '@') { moduleConfig = config.modules[moduleName] || {}; // Handle private modules. if (componentName[0] == '@') { var originalComponentName = componentName; var parts = componentName.split('/'); // windows??? delete config.modules[moduleName]; delete config.components[componentName]; componentName = parts[1]; moduleName = parts[1]; config.components[componentName] = componentConfig; componentConfig.moduleName = moduleName; moduleConfig = config.modules[moduleName] || moduleConfig; // TODO: resolve issues that arrise since because path needs to sometimes specify ClassName // eg path: '@private/module-name.ClassName' if (!moduleConfig.path) { moduleConfig.path = originalComponentName; } } } else if (!moduleConfig) { moduleConfig = config.modules[moduleName] = {}; } return _this._createElement({ component: { name: componentName, config: componentConfig, }, module: { name: moduleName, config: moduleConfig } }, false, callback); } } ), function (e) { callback(e, _this); }); }; Mesh.prototype._destroyElement = Promise.promisify(function (componentName, callback) { this.log.$$DEBUG('destroying element with component \'%s\'', componentName); var cached = true; var description = this.describe(cached).components[componentName]; var element = this._mesh.elements[componentName]; var endpointName = this._mesh.config.name; var endpoint = this._mesh.endpoints[endpointName]; var queue = this._mesh.destroyingElementQueue = this._mesh.destroyingElementQueue || []; var config = this._mesh.config; var _this = this; async.series([ // (Queue) Only allow 1 destroy at a time. // - gut feel, might not be necesssary. function (done) { var interval; if (_this._mesh.destroyingElement) { queue.push(componentName); interval = setInterval(function () { // ensure deque in create order if (!_this._mesh.destroyingElement && queue[0] == componentName) { clearInterval(interval); queue.shift(); _this._mesh.destroyingElement = componentName; done(); // proceed } }, 200); return; } _this._mesh.destroyingElement = componentName; done(); }, function (done) { endpoint.previousDescription = endpoint.description; var writeSchema = true; // transmit description to subscribers // (browsers, other mesh nodes) element.deleted = true; // flag informs describe() to no longer list // the element being destroyed _this._registerSchema(config, writeSchema, done); }, // TODO: Wait for clients/browser to remove functionality // from their APIs (per new descripition). // // Necessary? // // Is there a simple/'network lite' way to know // when they are all done? function (done) { done(); }, // Unclear... Question: // // - Should the component stopMethod be called before or after detaching it from the mesh. // - Similar to startMethod, i think the case can be made for both. Suggesting 2 kinds of // stopMethods: // // beforeDetach: can still tell clients // afterDetach: final cleanup, // no longer clients present to affect state/data during cleanup // // Decided to put it after detach for now. It can still 'talk' to other mesh components // viw $happn, but other mesh components can no longer talk to it through $happn // // // Stop the component. // function(done) { // done(); // }, function (done) { Internals._updateEndpoint(_this, endpointName, _this.exchange, _this.event, done); }, function (done) { _this._mesh.elements[componentName].component.instance._detatch(_this._mesh, done); // done(); }, function (done) { // TODO: Unregister from security, // TODO: Remove registered events and data paths. done(); }, // Stop the component if it was started and has a stopMethod function (done) { if (!_this._mesh.started) return done(); if (!element.component.config.stopMethod) return done(); _this._eachComponentDo({ methodCategory: 'stopMethod', targets: [componentName], logAction: 'stopped' }, done); }, function (done) { delete _this._mesh.elements[componentName]; // TODO: double-ensure no further refs remain // // ie. Rapidly adding and removing components is a likely use case. // All refs must be gone for garbage collection // done(); }, ], function (e) { delete _this._mesh.destroyingElement; // done, (interval above will proceed) callback(e); }); }); Mesh.prototype._createElement = Promise.promisify(function (spec, writeSchema, callback) { var _this = this; var config = _this._mesh.config; var endpointName = _this._mesh.config.name; if (typeof writeSchema == 'function') { callback = writeSchema; writeSchema = true; } _this.log.$$TRACE('_createElement( spec', spec); _this.log.$$DEBUG( 'creating element with component \'%s\' on module \'%s\' with writeSchema %s', spec.component.name, spec.module.name, writeSchema ); async.series([ function (done) { _this._createModule(spec, done); }, function (done) { _this._happnizeModule(spec, done); }, function (done) { _this._originizeModule(spec, done); }, function (done) { _this._createComponent(spec, done); }, function (done) { if (_this._mesh.initialized) { // Don't publish the schema if the mesh is already running // because then clients get the description update (new component) // before the component has been started. // // It is still necessary to refresh the description to that the // api can be built for the new component. // return _this._registerSchema(config, false, done); } _this._registerSchema(config, writeSchema, done); }, function (done) { Internals._updateExchangeAPILayer(_this, endpointName, _this.exchange, done); }, function (done) { Internals._updateEventAPILayer(_this, endpointName, _this.event, done); }, function (done) { if (!_this._mesh.started) return done(); // New component, mesh already started if (!spec.component.config.startMethod) return done(); _this._eachComponentDo({ methodCategory: 'startMethod', targets: [spec.component.name], // only start this component logAction: 'started' }, done); }, function (done) { if (_this._mesh.initialized) { // component has started, publish schema return _this._registerSchema(config, writeSchema, done); } done(); } ], function (e) { callback(e) }); }); Mesh.prototype._createModule = function (spec, callback) { // NB: If this functionality is moved into another module the // module.paths (as adjusted in updateSearchPath(caller)) // will need to be done in the new module. // // Otherwise the require() won't search from the caller's // perspective var _this = this; var moduleName = spec.module.name; var moduleConfig = spec.module.config; // // if (spec.module.config.packageName && this._mesh.config.packaged[spec.module.config.packageName]) // spec.module.packaged = this._mesh.config.packaged[spec.module.config.packageName]; // // spec.module.packaged isn't used anywhere... // var modulePath; var moduleBasePath; var pathParts; var callbackIndex = -1; var moduleInstance; var moduleBase; var ignore; var home; if (moduleConfig.instance) { moduleConfig.home = moduleConfig.home || '__NONE__'; moduleBase = moduleConfig.instance; home = moduleConfig.home; } if (!moduleConfig.path) moduleConfig.path = moduleName; try { modulePath = moduleConfig.path; if (moduleConfig.path.indexOf('system:') == 0) { pathParts = moduleConfig.path.split(':'); modulePath = __dirname + '/modules/' + pathParts[1]; } if (!home) { try { var dirName = path.dirname(modulePath); var baseName = path.basename(modulePath); baseName.replace(/\.js$/, '').split('.').map(function (part, ind) { if (ind == 0) { if (dirName == '.' && modulePath[0] != '.') { moduleBasePath = part; } else { moduleBasePath = part = dirName + path.sep + part; } _this.log.$$TRACE('requiring module %s', modulePath); moduleBase = require(part); } else moduleBase = moduleBase[part]; }); } catch (e) { try { _this.log.$$TRACE('alt-requiring module happner-%s', modulePath); moduleBase = require('happner-' + modulePath); moduleBasePath = 'happner-' + modulePath; } catch (f) { _this.log.$$TRACE('alt-requiring happner-%s failed', modulePath, f); throw e } } } home = home || path.dirname(require.resolve(moduleBasePath)); Object.defineProperty(spec.module, 'directory', { get: function () { if (depWarned3) return home; _this.log.warn('Use of module.directory is deprecated. Use module.home'); try { _this.log.warn(' - at %s', _this.getCallerTo('mesh.js')); } catch (e) { } depWarned3 = true; return home; } }); Object.defineProperty(spec.module, 'home', { get: function () { return home; } }); } catch (e) { return callback(e); } var getParameters = function () { try { if (!moduleConfig.construct && !moduleConfig.create) return []; var parameters = (moduleConfig.construct || moduleConfig.create).parameters; return parameters.map(function (p, i) { if (p.parameterType == 'callback') { callbackIndex = i; return; } if (p.value) return p.value; else return null }); } catch (e) { return []; } } var errorIfNull = function (module) { if (!module) { _this.log.warn('missing or null module \'%s\'', moduleName); return {}; } return module; } var parameters = getParameters(); if (moduleConfig.construct) { _this.log.$$TRACE('construct module \'%s\'', moduleName); if (moduleConfig.construct.name) moduleBase = moduleBase[moduleConfig.construct.name]; try { moduleInstance = new (Function.prototype.bind.apply(moduleBase, [null].concat(parameters))); spec.module.instance = errorIfNull(moduleInstance); } catch (e) { _this.log.error('error constructing \'%s\'', moduleName, e); return callback(e); } return callback(); } if (moduleConfig.create) { _this.log.$$TRACE('create module \'%s\'', moduleName); if (moduleConfig.create.name) moduleBase = moduleBase[moduleConfig.create.name]; if (moduleConfig.create.type != 'async') { var moduleInstance = moduleBase.apply(null, parameters); spec.module.instance = errorIfNull(moduleInstance); return callback(); } var constructorCallBack = function () { var callbackParameters; try { callbackParameters = moduleConfig.create.callback.parameters; } catch (e) { callbackParameters = [ {parameterType: 'error'}, {parameterType: 'instance'} ]; } for (var index in arguments) { var value = arguments[index]; var callBackParameter = callbackParameters[index]; if (callBackParameter.parameterType == 'error' && value) { return callback(new MeshError('Failed to construct module: ' + moduleName, value)); } if (callBackParameter.parameterType == 'instance' && value) { spec.module.instance = errorIfNull(value); return callback(); } } } if (callbackIndex > -1) parameters[callbackIndex] = constructorCallBack; else parameters.push(constructorCallBack); return moduleBase.apply(moduleBase, parameters); } if (typeof moduleBase == 'function') { _this.log.$$TRACE('construct/create module \'%s\'', moduleName); try { moduleInstance = new (Function.prototype.bind.apply(moduleBase, [null].concat(parameters))); } catch (e) { _this.log.error('error construct/creating \'%s\'', moduleName, e); return callback(e); } spec.module.instance = errorIfNull(moduleInstance); return callback(); } _this.log.$$TRACE('assign module \'%s\'', moduleName); spec.module.instance = errorIfNull(moduleBase); return callback(); }; Mesh.prototype._happnizeModule = function (spec, callback) { var args, happnSeq, originalFn; var module = spec.module.instance; for (var fnName in module) { originalFn = module[fnName]; if (typeof originalFn !== 'function') continue; args = utilities.getFunctionParameters(originalFn); happnSeq = args.indexOf('$happn'); if (happnSeq < 0) continue; Object.defineProperty(module[fnName], '$happnSeq', {value: happnSeq}); } callback(null); }; Mesh.prototype._originizeModule = function (spec, callback) { var args, originSeq, originalFn; var module = spec.module.instance; for (var fnName in module) { originalFn = module[fnName]; if (typeof originalFn !== 'function') continue; args = utilities.getFunctionParameters(originalFn); originSeq = args.indexOf('$origin'); if (originSeq < 0) continue; Object.defineProperty(module[fnName], '$originSeq', {value: originSeq}); } callback(null); }; Mesh.prototype._createComponent = function (spec, callback) { var _this = this; var config = _this._mesh.config; var componentName = spec.component.name; var componentConfig = spec.component.config; var componentInstance = new ComponentInstance(); if (!componentConfig.meshName) Object.defineProperty(componentConfig, 'meshName', { get: function () { if (depWarned1) return config.name; _this.log.warn('use of $happn.config.meshName is deprecated, use $happn.info.mesh.name'); try { _this.log.warn(' - at %s', _this.getCallerTo('mesh.js')); } catch (e) { } depWarned1 = true; return config.name; } }); if (!componentConfig.setOptions) Object.defineProperty(componentConfig, 'setOptions', { get: function () { if (depWarned2) return config.datalayer.setOptions; _this.log.warn('use of $happn.config.setOptions is deprecated, use $happn.info.datalayer.options'); try { _this.log.warn(' - at %s', _this.getCallerTo('mesh.js')); } catch (e) { } depWarned2 = true; return config.datalayer.setOptions; } }); _this.log.$$TRACE('created component \'%s\'', componentName, componentConfig); _this._stats.component[componentName] = {errors: 0, calls: 0, emits: 0}; componentInstance.stats = _this._stats; // TODO?: rather let the stats collector component // have accessLevel: 'mesh' (config in component) // and give each component it's own private stats store var __addComponentDataStoreRoutes = function (meshConfig, componentConfig) { if (!componentConfig.data || !componentConfig.data.routes) return; for (var route in componentConfig.data.routes) { var componentRoute = '/_data/' + componentName + '/' + route.replace(/^\//, ''); _this._mesh.datalayer.server.services.data.addDataStoreFilter(componentRoute, componentConfig.data.routes[route]); } }.bind(_this); //TODO - we need to remove routes somewhere too: // var __removeComponentDataStoreRoutes = function(meshConfig, componentConfig){ // if (componentConfig.data && // meshConfig.data && // meshConfig.data.persist){//persist is set to true if datalayer.filename is set, or is also explicitly set // for (var route in componentConfig.data.routes){ // var componentRoute = '/_data/' + componentName + '/' + route.replace(/^\//,''); // console.log('adding route:::', componentRoute); // this._mesh.datalayer.server.services.data.removeDataStoreFilter(componentRoute, componentConfig.data.routes[route]); // } // } // }.bind(_this); componentInstance.initialize( componentName, root, _this, spec.module, componentConfig, function (e) { if (e) return callback(e); __addComponentDataStoreRoutes(config, componentConfig); spec.component.instance = componentInstance; _this._mesh.elements[componentName] = spec; callback(); } ); }; Mesh.prototype._registerInitialSchema = function (config, callback) { // The datalayer is up and waiting for connections before there has been any // description written, remote mesh nodes or clients that attach after // the datalater init but before the schema is registered get a null description // and become dysfunctional. // // This registers an initial description marked with initializing = true, // the remote has subscribed to further description updates from here, so // it will receive the full description (initializing = false) as soon as this // meshnode completes it's initialization. // // The remote does not callback from it's _initializeEndpoints until a full // description from each endpoint has arrived there. // // // This is to ensure that ALL functionality EXPECTED by the remote on it's // endpoint is present and ready at callback (that no component on the remote // is still loading) // // // This above behaviour needs to be configurable. // ---------------------------------------------- // // // As it stands if all meshnodes start simultaneously, then each takes as // long as it's slowest endpoint to callback from initialize() // // Cannot use this.description(), it requires bits not present yet var _this = this; var description = { initializing: true, name: _this._mesh.config.name, components: {}, }; _this._mesh.data.set('/mesh/schema/description', description, function (e, response) { if (e) return callback(e); _this._mesh.data.set('/mesh/schema/config', _this._filterConfig(config), function (e, response) { callback(e); }); }); }; Mesh.prototype._filterConfig = function (config) { //share only what is necessary var sharedConfig = {name:config.name, version:this.version, datalayer:{}}; sharedConfig.datalayer.port = config.datalayer.port; sharedConfig.datalayer.secure = config.datalayer.secure; sharedConfig.datalayer.encryptPayloads = config.datalayer.encryptPayloads; sharedConfig.datalayer.setOptions = config.datalayer.setOptions; sharedConfig.datalayer.transport = config.datalayer.transport; return sharedConfig; }; Mesh.prototype._registerSchema = function (config, writeSchema, callback) { // - writeSchema is set to false during intialization as each local component // init calls through here to update the description ahead of it's API // initializations. // // - This prevents a write to the datalayer for every new component. // // - New components added __during runtime__ using createElement() call // through here with write as true so that the description change // makes it's way to clients and other attached nodes. // var _this = this; var description = _this.describe(false, true); // Always use the cached component descriptions // to alleviate load as this function gets called // for every local component initialization _this.log.$$TRACE('_registerSchema( description with name: %s', description.name, writeSchema ? description : null); if (!writeSchema) return callback(); // TODO: only write if there actually was a change. _this._mesh.data.set('/mesh/schema/description', description, function (e, response) { if (e) return callback(e); _this._mesh.data.set('/mesh/schema/config', _this._filterConfig(config), function (e, response) { callback(e); }); }); }; Mesh.prototype._initializeEndpoints = function (callback) { var _this = this; var config = _this._mesh.config; // Externals var exchangeAPI = _this.exchange = (_this.exchange || {}); var eventAPI = _this.event = (_this.event || {}); // Internals _this._mesh = _this._mesh || {}; _this._mesh.exchange = _this._mesh.exchange || {}; async.parallel(Object.keys(config.endpoints).map(function (endpointName) { // return array of functions for parallel([]) return function (done) { _this.log.$$DEBUG('initialize endpoint \'%s\'', endpointName); var calledBack = false; var endpointConfig = config.endpoints[endpointName]; endpointConfig.config = endpointConfig.config || {}; _this.log.$$TRACE('Happn.client.create( ', endpointConfig); endpointConfig.info = endpointConfig.info || {}; endpointConfig.info.mesh = endpointConfig.info.mesh || {}; endpointConfig.info.mesh.name = endpointConfig.info.mesh.name || _this._mesh.config.name; // TODO: Shouldn't this rather use MeshClient instead of happn.client directly. // TODO: Configurable 'wait time' for full description // TODO: Configurable stop or carry-on-regardless on timeout if (!endpointConfig.config.keyPair) endpointConfig.config.keyPair = _this._mesh.datalayer.server.services.security._keyPair; Happn.client.create(endpointConfig, function (error, client) { var description; if (error) { _this.log.error('failed connection to endpoint \'%s\'', endpointName, error); return done(error); } client.__endpointConfig = endpointConfig; client.__endpointName = endpointName; //attach to the happn clients connection status events, the data is bubbled up through mesh events client.onEvent('connection-ended', function (eventData) { _this.emit('endpoint-connection-ended', { endpointConfig: client.__endpointConfig, endpointName: client.__endpointName, eventData: eventData }); }); client.onEvent('reconnect-scheduled', function (eventData) { _this.emit('endpoint-reconnect-scheduled', { endpointConfig: client.__endpointConfig, endpoint