happner
Version:
distributed application engine with evented storage and mesh services
502 lines (361 loc) • 15.6 kB
JavaScript
;(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);
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.__createCallbackHandler = function (actualHandler, message) {
var _this = this;
var callbackHandler = {
"handler": actualHandler,
"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, this.handler.toString());
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 || 10000);
return callbackHandler;
}
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');
_this.responseHandlers[message.callbackAddress] = _this.__createCallbackHandler(args[index], message);
}
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]);
}
});
// if the method is sync on the other side with no result expected, we don't create a callback handler
if (args.length > 0 && methodDescription.type != 'sync' &&
typeof args[args.length - 1] == 'function' && !_this.responseHandlers[message.callbackAddress]) {
_this.responseHandlers[message.callbackAddress] = _this.__createCallbackHandler(args[args.length - 1], message);
}
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 (serializedError 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.log.$$DEBUG('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');
}
};
})(typeof module !== 'undefined' && typeof module.exports !== 'undefined' ? false : true);