happner
Version:
distributed application engine with evented storage and mesh services
911 lines (697 loc) • 27.1 kB
JavaScript
var MeshError = require('./shared/mesh-error');
var serveStatic = require('serve-static');
var path = require('path');
var depWarned0 = false; // $happn.mesh.*
var fs = require('fs');
var utilities = require('./utilities');
module.exports = ComponentInstance;
function ComponentInstance() {
}
ComponentInstance.prototype.secureData = function (meshData, componentName) {
var securedMeshData = {};
securedMeshData.__persistedPath = '/_data/' + componentName;
securedMeshData.getPath = function (path) {
if (!path)
throw new Error('invalid path: ' + path);
if (path[0] != '/')
path = '/' + path;
return this.__persistedPath + path;
};
securedMeshData.on = function (path, parameters, handler, callback) {
return meshData.on(this.getPath(path), parameters, handler, callback);
};
securedMeshData.off = function (listenerRef, callback) {
if (typeof listenerRef == "number")
return meshData.off(listenerRef, callback);
return meshData.off(this.getPath(listenerRef), callback);
};
securedMeshData.offAll = function (callback) {
//we cannot do a true offAll, otherwise we get no message back
return meshData.offPath(this.getPath('*'), callback);
};
securedMeshData.offPath = function (path, callback) {
return meshData.offPath(this.getPath(path), callback);
};
securedMeshData.get = function (path, parameters, callback) {
return meshData.get(this.getPath(path), parameters, callback);
};
securedMeshData.getPaths = function (path, callback) {
return meshData.getPaths(this.getPath(path), callback);
};
securedMeshData.set = function (path, data, parameters, callback) {
return meshData.set(this.getPath(path), data, parameters, callback);
};
securedMeshData.setSibling = function (path, data, callback) {
return meshData.setSibling(this.getPath(path), data, callback);
};
securedMeshData.remove = function (path, parameters, callback) {
return meshData.remove(this.getPath(path), parameters, callback);
};
return securedMeshData;
};
ComponentInstance.prototype.initialize = function (name, root, mesh, module, config, callback) {
this.README = this.README; // make visible
this.name = name;
this.config = {};
this.info = {
mesh: {}, // name,
datalayer: {}, // address, options
};
this.log = mesh._mesh.log.createLogger(this.name);
this.Mesh = require('../mesh'); // local Mesh definition avaliable on $happn
this.log.$$DEBUG('create instance');
var _this = this;
Object.defineProperty(_this, 'mesh', {
get: function () {
if (depWarned0) return _this;
_this.log.warn('Use of $happn.mesh.* is deprecated. Use $happn.*');
try {
_this.log.warn(' - at %s', mesh.getCallerTo('componentInstance.js'));
} catch (e) {
}
depWarned0 = true;
return _this;
}
});
Object.defineProperty(_this, 'tools', {
get: function () {
return mesh.tools;
},
enumerable: true,
})
var defaults;
//TODO, here are the module.packaged settings
if (typeof (defaults = module.instance.$happner) == 'object') {
if (defaults.config && defaults.config.component) {
Object.keys(defaults = defaults.config.component)
.forEach(function (key) {
// - Defaulting applies only to the 'root' keys nested immediately
// under the 'component' config
// - Does not merge.
// - Each key in the default is only used if the corresponding key is
// not present in the inbound config.
if (typeof config[key] == 'undefined') {
config[key] = JSON.parse(JSON.stringify(defaults[key]));
}
})
}
}
Object.defineProperty(this.info.mesh, 'name', {
enumerable: true,
value: mesh._mesh.config.name,
});
Object.defineProperty(this.info.datalayer, 'address', {
enumerable: true,
get: function () {
var address = mesh._mesh.datalayer.server.server.address();
// TODO: ideally this value would come from the actual server, not the config
address.protocol = mesh._mesh.config.datalayer.transport.mode || 'http';
return address;
}
});
Object.defineProperty(this.info.datalayer, 'options', {
enumerable: true,
get: function () {
return mesh._mesh.config.datalayer.setOptions; // TODO: should rather point to actual datalayer options,
// it may have defaulted more than we passed in.
}
});
if (mesh._mesh.serializer) {
Object.defineProperty(this, 'serializer', {
value: mesh._mesh.serializer
});
}
if (config.accessLevel == 'root') {
Object.defineProperty(this, '_root', {
get: function () {
return root
},
enumerable: true
});
}
if (config.accessLevel == 'mesh' || config.accessLevel == 'root') {
Object.defineProperty(this, '_mesh', {
get: function () {
return mesh._mesh
},
enumerable: true
});
}
try {
this.config = config;
this.exchange = mesh.exchange;
this.event = mesh.event;
this.data = this.secureData(mesh._mesh.data, this.name);
this._loadModule(module);
this._attach(config, mesh._mesh, callback);
} catch (err) {
callback(new MeshError('Failed to initialize component', err));
}
};
/*
ComponentInstance.prototype.emit = function(key, data, callback){
this.stats.component[this.name].emits++;
var eventKey = '/events/' + this.info.mesh.name + '/' + this.name + '/';
this.data.set(eventKey + key, data, {noStore:true}, callback);
}
*/
ComponentInstance.prototype.describe = function (cached) {
var _this = this;
if (!cached || !this.description) {
Object.defineProperty(this, 'description', {
value: {
name: _this.name,
methods: {},
routes: {},
},
configurable: true
});
// build description.routes (component's web routes)
var webMethods = {}; // accum list of webMethods to exclude from exhange methods description
var routes, defn;
if (this.config.web && (routes = this.config.web.routes)) {
for (var routePath in routes) {
var route = routes[routePath];
if (route instanceof Array) {
route.forEach(function (method) {
webMethods[method] = 1;
route = method; // last in route array is used to determine type: static || mware
});
} else {
webMethods[route] = 1;
}
if (this.name == 'www' && routePath == 'global') continue;
if (this.name == 'www') {
if (routePath == 'static') {
routePath = '/'; // www: static: ['web','route'] served at /
}
else {
routePath = '/' + routePath; // www: webMethod: served at /webMethod
// www: other: 'static' served at /other
}
}
else if (routePath == 'resources' && this.name == 'resources') {
routePath = '/' + routePath;
}
else if (routePath == 'static') {
routePath = '/';
}
else {
routePath = '/' + this.name + '/' + routePath;
}
// TODO: Not advertizing mesh route (eg. /meshname/component/method)
// No point.
// I think it would be better to remove the meshroutes alltogether
defn = this.description.routes[routePath] = {};
defn.type = route == 'static' ? 'static' : 'mware';
// TODO: Advertize interface, params, query, restness, hmm
}
}
// build description.events (components events)
if (this.config.events) {
this.description.events = JSON.parse(JSON.stringify(this.config.events));
}
else {
this.description.events = {
/* default none */
};
}
// build description.data (components store paths)
//
// TODO: Build the per component data api wrapper that prepends the datapaths
// with something like _DATA/componentName/... so that security can be
// applied on a per component basis. (keep in mind folks may also subscribe
// to their data paths)
if (this.config.data) {
this.description.data = JSON.parse(JSON.stringify(this.config.data));
}
else {
this.description.data = {
/* default none */
};
}
// build description.methods (component's web routes)
var getMethodDefn = function (config, methodName) {
if (!config.schema) return;
if (!config.schema.methods) return;
if (!config.schema.methods[methodName]) return;
return config.schema.methods[methodName];
}
for (var methodName in this.module.instance) {
var method = this.module.instance[methodName];
var methodDefined = getMethodDefn(this.config, methodName);
if (typeof method !== 'function') continue;
if (method.$happner && method.$happner.ignore && !methodDefined) continue;
if (methodName.indexOf('_') == 0 && !methodDefined) continue;
if (!this.config.schema || (this.config.schema && !this.config.schema.exclusive)) {
// no schema or not exclusive, allow all (except those filtered above and those that are webMethods)
if (webMethods[methodName]) continue;
this.description.methods[methodName] = methodDefined = methodDefined || {};
if (!methodDefined.parameters) {
this._defaultParameters(method, methodDefined);
}
continue;
}
if (methodDefined) {
// got schema and exclusive is true (per filter in previous if) and have definition
this.description.methods[methodName] = methodDefined;
if (!methodDefined.parameters) {
this._defaultParameters(method, methodDefined);
}
}
}
}
return this.description;
};
ComponentInstance.prototype._inject = function(methodDefn, parameters, origin) {
// must inject left to right otherwise subsequent left inject slides preceding right inject rightward
if (typeof methodDefn.$happnSeq !== 'undefined' && typeof methodDefn.$originSeq !== 'undefined') {
if (methodDefn.$happnSeq < methodDefn.$originSeq) {
parameters.splice(methodDefn.$happnSeq, 0, this);
parameters.splice(methodDefn.$originSeq, 0, origin);
return;
}
parameters.splice(methodDefn.$originSeq, 0, origin);
parameters.splice(methodDefn.$happnSeq, 0, this);
return;
}
if (typeof methodDefn.$happnSeq !== 'undefined') parameters.splice(methodDefn.$happnSeq, 0, this);
if (typeof methodDefn.$originSeq !== 'undefined') parameters.splice(methodDefn.$originSeq, 0, origin);
};
ComponentInstance.prototype._loadModule = function (module) {
var _this = this;
Object.defineProperty(this, 'module', { // property: to remove internal components from view.
value: module
});
Object.defineProperty(this, 'operate', {
value: function (methodName, parameters, callback, origin) {
try {
var callbackIndex = -1;
var callbackCalled = false;
_this.stats.component[_this.name].calls++;
var methodSchema = _this.description.methods[methodName];
var methodDefn = _this.module.instance[methodName];
var error;
if (!methodSchema) {
error = new Error('Call to unconfigured method \'' + _this.name + '.' + methodName + '()\'');
_this.log.error('Missing method config', error);
if (callback) return callback(error);
// throw error
return;
}
_this.log.$$TRACE('operate( %s', methodName);
_this.log.$$TRACE('parameters ', parameters);
_this.log.$$TRACE('methodSchema ', methodSchema);
if (typeof methodDefn !== 'function')
throw new MeshError('Missing ' + _this.name + '.' + methodName + '()', {parameters: parameters});
if (callback) {
if (methodSchema.type == 'sync-promise') {
try {
_this._inject(methodDefn, parameters, origin);
var result = methodDefn.apply(_this.module.instance, parameters);
return callback(null, [null, result]);
} catch (e) {
return callback(null, [e]);
}
}
for (var i in methodSchema.parameters) {
if (methodSchema.parameters[i].type == 'callback') callbackIndex = i
}
var callbackProxy = function () {
if (callbackCalled) return _this.log.error('Callback invoked more than once for method %s', methodName);
callbackCalled = true;
callback(null, Array.prototype.slice.apply(arguments));
};
if (callbackIndex == -1){
parameters.push(callbackProxy)
}
else {
parameters.splice(callbackIndex, 1, callbackProxy);
}
}
_this._inject(methodDefn, parameters, origin);
var returnObject = methodDefn.apply(_this.module.instance, parameters);
if (callbackIndex == -1 && utilities.isPromise(returnObject)) {
returnObject
.then(function (result) {
if (callbackProxy) callbackProxy(null, result);
})
.catch(function (err) {
if (callbackProxy) callbackProxy(err);
});
}
} catch (e) {
_this.log.error('Call to method %s failed', methodName, e);
_this.stats.component[_this.name].errors++;
if (callback)
callback(e);
//else throw error;
//
//TODO - for syncronous calls may still want to throw, but it takes down the mesh
}
}
});
};
ComponentInstance.prototype._defaultParameters = function (method, methodSchema) {
if (!methodSchema.parameters) methodSchema.parameters = [];
utilities.getFunctionParameters(method)
.filter(function (argName) {
return argName != '$happn' && argName != '$origin';
})
.map(function (argName) {
methodSchema.parameters.push({name: argName});
});
};
ComponentInstance.prototype._discardMessage = function (reason, message) {
this.log.error("message discarded: %s", reason, message);
};
ComponentInstance.prototype.attachRouteTarget = function (mesh, meshRoutePath, componentRoutePath, targetMethod, route) {
var directory, serve, methodDefn = this.module.instance[targetMethod];
var connect = mesh.datalayer.server.connect;
if (targetMethod == 'static') {
if (this.module.home == '__NONE__') return;
/////////////////////////////// still necessary?
if (this.name == 'resources' && route == 'resources') {
directory = path.normalize(__dirname + '/../../resources/');
this.log.$$TRACE('serving static %s', directory);
this.log.$$TRACE(' - at %s', componentRoutePath);
this.log.$$TRACE(' - at %s', meshRoutePath);
serve = serveStatic(directory);
serve.__tag = this.name; // tag each middleware with component name
// for ability to remove from connect stack
connect.use(meshRoutePath.replace(/\/resources$/, ''), serve);
connect.use(componentRoutePath.replace(/\/resources$/, ''), serve);
return;
}
;
/////////////////////////////// still necessary?
directory = this.module.home + '/' + route;
if (this.name == 'www') {
if (route == 'static') {
componentRoutePath = '/';
this.log.$$TRACE('serving www %s', directory);
this.log.$$TRACE(' - at %s', componentRoutePath);
serve = serveStatic(directory);
serve.__tag = this.name;
connect.use(componentRoutePath, serve);
return;
}
componentRoutePath = '/' + route;
this.log.$$TRACE('serving www %s', directory);
this.log.$$TRACE(' - at %s', componentRoutePath);
serve = serveStatic(directory);
serve.__tag = this.name;
connect.use(componentRoutePath, serve);
return;
}
this.log.$$TRACE('serving static %s', directory);
this.log.$$TRACE(' - at %s', componentRoutePath);
this.log.$$TRACE(' - at %s', meshRoutePath);
serve = serveStatic(directory);
serve.__tag = this.name;
connect.use(meshRoutePath, serve);
connect.use(componentRoutePath, serve);
return;
}
if (route == 'static' && this.name != 'www') {
var isDir = false;
try {
isDir = fs.lstatSync(targetMethod).isDirectory()
} catch (e) {
//thats ok, wasnt a directory
}
if (isDir)
directory = targetMethod;
else
directory = this.module.home + '/' + targetMethod;
//we are serving static content, straight from the component, so remove the targetMethod
componentRoutePath = componentRoutePath.replace('/static', '');
meshRoutePath = meshRoutePath.replace('/static', '');
this.log.$$TRACE('serving static target %s', directory);
this.log.$$TRACE('targetMethod %s', targetMethod);
this.log.$$TRACE(' - at %s', componentRoutePath);
this.log.$$TRACE(' - at %s', meshRoutePath);
serve = serveStatic(directory);
serve.__tag = this.name;
connect.use(meshRoutePath, serve);
connect.use(componentRoutePath, serve);
return;
}
if (typeof methodDefn != 'function') {
throw new MeshError('Expected "' + this.name + '" to define ' + targetMethod + '()')
}
var context = this.module.instance;
var _this = this;
if (typeof methodDefn.$happnSeq !== 'undefined' || typeof methodDefn.$originSeq !== 'undefined') {
var securityService = mesh.datalayer.server.services.security;
serve = function () {
var args = Array.prototype.slice.call(arguments);
if (methodDefn.$happnSeq !== 'undefined') args.splice(methodDefn.$happnSeq, 0, _this);
if (methodDefn.$originSeq){
if (!mesh.config.datalayer.secure) args.splice(methodDefn.$originSeq, 0, null);
else args.every(function(arg){
if (arg.method != null && arg.url){
try{
var query = require('url').parse(arg.url, true).query;
if (query.happn_token){
var session = securityService.decodeToken(query.happn_token);
//var session = pubsubService.getSession(decodedToken.id);
if (!session) throw new Error('sesson expired for token ' + query.happn_token);
args.splice(methodDefn.$originSeq, 0, session);
}
}catch(e){
_this.log.warn('failed to map incoming request token to $origin');
args.splice(methodDefn.$originSeq, 0, null);
}
return false;
} else return true;
});
}
methodDefn.apply(context, args);
};
serve.__tag = this.name;
if (this.name == 'www') {
if (route == 'global') {
this.log.$$TRACE('global $happn method %s()', targetMethod);
connect.use(serve);
return;
}
if (route == 'static') {
componentRoutePath = '/';
}
else {
componentRoutePath = '/' + route;
}
this.log.$$TRACE('serving $happn method %s()', targetMethod);
this.log.$$TRACE('- at ' + componentRoutePath);
connect.use(componentRoutePath, serve);
return;
}
connect.use(meshRoutePath, serve);
connect.use(componentRoutePath, serve);
this.log.$$TRACE('serving $happn method %s()', targetMethod);
this.log.$$TRACE(' - at %s', componentRoutePath);
this.log.$$TRACE(' - at %s', meshRoutePath);
return;
}
if (this.name == 'www') {
if (route == 'global') {
this.log.$$TRACE('global _ method %s()', targetMethod);
serve = methodDefn.bind(context);
serve.__tag = this.name;
connect.use(serve);
return;
}
componentRoutePath = '/' + route;
this.log.$$TRACE('serving _ method %s()', targetMethod);
this.log.$$TRACE('- at %s', componentRoutePath);
}
this.log.$$TRACE('serving _ method %s()', targetMethod);
this.log.$$TRACE(' - at %s', componentRoutePath);
this.log.$$TRACE(' - at %s', meshRoutePath);
serve = methodDefn.bind(context);
serve.__tag = this.name;
connect.use(meshRoutePath, serve);
connect.use(componentRoutePath, serve);
};
ComponentInstance.prototype._attach = function (config, mesh, callback) {
//attach module to the transport layer
this.log.$$DEBUG('_attach()');
var _this = this;
_this.emit = function (key, data, callback) {
_this.stats.component[_this.name].emits++;
var eventKey = '/_events/' + _this.info.mesh.name + '/' + _this.name + '/';
mesh.data.set(eventKey + key, data, {noStore: true}, callback);
};
if (config.web) {
try {
for (var route in config.web.routes) {
var routeTarget = config.web.routes[route];
var meshRoutePath = '/' + this.info.mesh.name + '/' + this.name + '/' + route;
var componentRoutePath = '/' + this.name + '/' + route;
if (Array.isArray(routeTarget)) {
routeTarget.map(function (targetMethod) {
_this.attachRouteTarget(mesh, meshRoutePath, componentRoutePath, targetMethod, route);
});
} else {
this.attachRouteTarget(mesh, meshRoutePath, componentRoutePath, routeTarget, route);
}
}
} catch (e) {
this.log.error("Failure to attach web methods", e);
return callback(e);
}
}
var listenAddress = '/_exchange/requests/' + this.info.mesh.name + '/' + this.name + '/';
var subscribeMask = listenAddress + '*';
_this.log.$$TRACE('data.on( ' + subscribeMask);
mesh.data.on(subscribeMask, {event_type: 'set'}, function (publication, meta) {
_this.log.$$TRACE('received request at %s', subscribeMask);
var pathParts = meta.path.replace(listenAddress, '').split('/');
var message = publication;
var method = pathParts[0];
if (_this.serializer && typeof _this.serializer.__decode == 'function') {
message.args = _this.serializer.__decode(message.args, {
req: true,
res: false,
at: {
mesh: _this.info.mesh.name,
component: _this.name,
},
meta: meta,
});
}
var args = message.args.slice(0, message.args.length);
if (!message.callbackAddress) return _this._discardMessage('No callback address', message);
_this.operate(method, args, function (e, responseArguments) {
var serializedError;
if (e) {
// error objects cant be sent / received (serialize)
serializedError = {
message: e.message,
name: e.name,
}
Object.keys(e).forEach(function (key) {
serializedError[key] = e[key];
});
_this.log.$$TRACE('operate( data.set( ERROR %s', message.callbackAddress);
return mesh.data.set(message.callbackAddress, {
status: 'failed',
args: [serializedError]
}, _this.info.datalayer.options);
}
var response = {
status: 'ok',
args: responseArguments
};
if (responseArguments[0] instanceof Error) {
response.status = 'error';
var e = responseArguments[0];
serializedError = {
message: e.message,
name: e.name
};
Object.keys(e).forEach(function (key) {
serializedError[key] = e[key];
});
responseArguments[0] = serializedError;
}
if (_this.serializer && typeof _this.serializer.__encode == 'function') {
response.args = _this.serializer.__encode(response.args, {
req: false,
res: true,
src: {
mesh: _this.info.mesh.name,
component: _this.name
},
meta: meta,
opts: _this.info.datalayer.options
});
}
// Populate response to the callback address
_this.log.$$TRACE('operate( data.set( RESULT %s', message.callbackAddress);
mesh.data.set(message.callbackAddress, response, _this.info.datalayer.options, function (e) {
if (e) _this.log.error("Failure to set callback data", e);
});
},
message.origin);
},
function (e, r) {
callback(e);
}
);
}
ComponentInstance.prototype._detatch = function (mesh, callback) {
//
// mesh._mesh
this.log.$$DEBUG('_detach() removing component from mesh');
var _this = this;
var connect = mesh.datalayer.server.connect;
var name = this.name;
// Remove this component's middleware from the connect stack.
var toRemove = connect.stack
.map(function (mware, i) {
if (mware.handle.__tag != name) return -1;
return i;
})
.filter(function (i) {
return i >= 0;
})
// splice starting from the back end so that array size change does not offset
.reverse();
toRemove.forEach(function (i) {
_this.log.$$TRACE('removing mware at %s', connect.stack[i].route);
connect.stack.splice(i, 1);
});
// Remove this component's request listener from the datalayer
var listenAddress = '/_exchange/requests/' + this.info.mesh.name + '/' + this.name + '/';
var subscribeMask = listenAddress + '*';
_this.log.$$TRACE('removing request listener %s', subscribeMask);
mesh.data.offPath(subscribeMask, function (e, r) {
if (e) _this.log.warn(
'half detached, failed to remove request listener %s', subscribeMask, e
);
callback(e);
});
};
ComponentInstance.prototype.runTestInternal = function (callback) {
try {
if (!this.module.instance.runTest)
return callback(new MeshError('Module is not testable'));
var _this = this;
this.module.instance.runTest(callback);
this.operateInternal(data.message, data.parameters, function (e, result) {
if (e)
return callback(e);
if (_this.module.instance.verifyTestResults)
return _this.module.instance.verifyTestResults(result, callback);
callback(null, result);
});
} catch (e) {
callback(e);
}
};
// terminal: inline help $happn.README
ComponentInstance.prototype.README = function () {/*
</br>
## This is the Component Instance
It is available in the terminal at **$happn**. From modules, it is optionally
injected (by argument name) into functions as **$happn**.
It has access to the **Exchange**, **Event** and **Data** APIs as well as some
built in utilities and informations.
### Examples
__node> $happn.name
'terminal'
__node> $happn.constructor.name
'ComponentInstance'
__node> $happn.log.warn('blah blah')
**[ WARN]** - 13398ms home (terminal) blah blah
__node> $happn.info
__node> $happn.config
__node> $happn.data.README
__node> $happn.event.README
__node> $happn.exchange.README
__node> $happn._mesh.* // only with 'mesh'||'root' accessLevel
__node> $happn._root.* // only with 'root' accessLevel
*/
}