diffusion
Version:
Diffusion JavaScript client
737 lines (617 loc) • 25.5 kB
JavaScript
var topicSelectorParser = require('topics/topic-selector-parser');
var _implements = require('util/interface')._implements;
var Services = require('services/services');
var ConversationId = require('conversation/conversation-id');
var SessionId = require('session/session-id');
var CommandService = require('client/command-service');
var ControlGroup = require('control/control-group');
var registration = require('control/registration');
var DataTypes = require('data/datatypes');
var Emitter = require('events/emitter');
var Result = require('events/result');
var ErrorReason = require('../../errors/error-reason');
var api = require('../../features/messages');
var arrays = require('util/array');
var logger = require('util/logger').create('Session.Messages');
var requireNonNull = require('util/require-non-null');
// Used because of the serialised form of SessionSendRequest requires a CID
var dummyCID = ConversationId.fromString("0");
function createSendOptions(options) {
options = options || {};
options.headers = options.headers || [];
options.priority = options.priority || 0;
return options;
}
function MessageStream(e, selector) {
this.selector = selector;
e.assign(this);
}
function RequestStreamProxy(reqType, resType, stream) {
this.reqType = reqType;
this.resType = resType;
this.stream = stream;
}
function RequestResponder(callback, responseType) {
this.respond = function() {
if (arguments.length === 0) {
throw new Error('No argument provided to the responder');
}
var response = arguments[0];
if ((response === null || response === undefined) && !responseType) {
throw new Error('No response type has been provided and the value provided is null. ' +
'Unable to determine the response type.');
}
var dataType = responseType ? DataTypes.getChecked(responseType) : DataTypes.getByValue(response);
var responseData = dataType.writeValue(response);
callback.respond({
responseDataType : dataType.name(),
data : responseData
});
};
this.reject = function(message) {
callback.fail(ErrorReason.REJECTED_REQUEST, message);
};
}
// Returns a session ID if parseable, else false.
function parseSessionId(str) {
try {
var sid = SessionId.fromString(str);
return sid;
}
catch(err) {
return false;
}
}
module.exports = _implements(api, function MessagesImpl(internal) {
var serviceLocator = internal.getServiceLocator();
var sender = serviceLocator.obtain(Services.SEND);
var sessionSender = serviceLocator.obtain(Services.SEND_TO_SESSION);
var filterSender = serviceLocator.obtain(Services.SEND_TO_FILTER);
var MESSAGING_SEND = serviceLocator.obtain(Services.MESSAGING_SEND);
var MESSAGING_FILTER_SEND = serviceLocator.obtain(Services.MESSAGING_FILTER_SEND);
var MESSAGING_RECEIVER_SERVER = serviceLocator.obtain(Services.MESSAGING_RECEIVER_SERVER);
var deregisterRequestHandler = serviceLocator.obtain(Services.MESSAGING_RECEIVER_CONTROL_DEREGISTRATION);
var requestStreams = {};
var streams = [];
var serviceRegistry = internal.getServiceRegistry();
var formatNoStreamForPath = function(sessionID, path){
return "Session " + sessionID +
" has no registered streams for message sent to path '" +
path + "'";
};
serviceRegistry.add(Services.SEND, CommandService.create(function(internal, message, callback) {
var received = false;
streams.forEach(function(stream) {
if (stream.l.selector.selects(message.path)) {
stream.e.emit('message', message);
received = true;
}
});
if (received) {
callback.respond();
} else {
var e = formatNoStreamForPath(internal.getSessionId(), message.path);
logger.info(e);
callback.fail(ErrorReason.UNHANDLED_MESSAGE, e);
}
}));
serviceRegistry.add(Services.MESSAGING_SEND, CommandService.create(function(internal, request, callback) {
var proxy = requestStreams[request.path];
if (proxy === undefined) {
callback.fail(
ErrorReason.UNHANDLED_MESSAGE,
formatNoStreamForPath(internal.getSessionId(), request.path));
return;
}
var responder = new RequestResponder(callback, proxy.resType);
var requestDataType = request.dataType;
var requestType = proxy.reqType ? proxy.reqType : requestDataType.valueClass;
if (requestDataType.canReadAs(requestType)) {
var requestData;
try {
requestData = requestDataType.readAs(requestType, request.request);
} catch (e) {
logger.error("Failed to convert " + requestDataType.name() + " datatype to " + requestType);
proxy.stream.onError(ErrorReason.INVALID_DATA);
delete requestStreams[request.path];
return;
}
try {
proxy.stream.onRequest(request.path, requestData, responder);
} catch (e) {
logger.error("Messaging request stream threw an error", e);
callback.fail(ErrorReason.CALLBACK_EXCEPTION, e.stack);
}
} else {
var message =
"Messaging request for path " +
request.path +
" with " +
requestDataType.name() +
" datatype is incompatible for stream (" + requestType + ")";
logger.debug(message);
callback.fail(ErrorReason.INCOMPATIBLE_DATATYPE, message);
}
}));
serviceRegistry.add(Services.FILTER_RESPONSE, CommandService.create(function(internal, request, callback) {
callback.respond();
internal.getConversationSet().respond(request.cid, request);
}));
serviceRegistry.add(Services.SEND_RECEIVER_CLIENT,
CommandService.create(function(internal, request, callback) {
callback.respond();
internal.getConversationSet().respond(request.cid, request);
}));
serviceRegistry.add(Services.MESSAGING_RECEIVER_CLIENT,
CommandService.create(function(internal, request, callback) {
internal.getConversationSet().respond(request.cid, {
request: request,
callback: callback.respond,
fail: callback.fail
});
}));
this.addHandler = function(path, handler, keys) {
// The registration code will return it's own result - we only make our own for error propagation
var emitter = new Emitter();
var result = new Result(emitter);
if (!path) {
emitter.error(new Error('Message Handler path is null or undefined'));
return result;
}
if (!handler) {
emitter.error(new Error('Message Handler is null or undefined'));
return result;
}
if (internal.checkConnected(emitter)) {
logger.debug('Adding message handler', path);
var params = {
definition : Services.SEND_RECEIVER_CLIENT,
group : ControlGroup.DEFAULT,
path : path,
keys : keys !== undefined ? keys : {}
};
var adapter = {
active : function(close) {
logger.debug('Message handler active', path);
handler.onActive(close);
},
respond : function(response) {
logger.debug('Message handler response', path);
var message = {
session : response.sender.toString(),
content : response.message,
options : response.options,
path : response.path
};
// Only set properties if there are some present.
var properties = response.sessionProperties;
if (properties && Object.keys(properties).length > 0) {
message.properties = properties;
}
handler.onMessage(message);
return false;
},
close : function() {
logger.debug('Message handler closed', path);
handler.onClose();
}
};
return registration.registerMessageHandler(internal, params, adapter);
} else {
return result;
}
};
this.listen = function(path, cb) {
var emitter = new Emitter(undefined, undefined, ['message']);
var selector = topicSelectorParser(path);
var stream = new MessageStream(emitter, selector);
var ref = {
l : stream,
e : emitter
};
stream.on('close', function() {
arrays.remove(streams, ref);
});
if (cb && cb instanceof Function) {
stream.on('message', cb);
}
streams.push(ref);
return stream;
};
this.send = function(path, message, options, recipient) {
var emitter = new Emitter();
var result = new Result(emitter);
var sendCallback = function(err, result) {
if (err) {
logger.debug('Message send failed', path);
emitter.error(err);
} else {
logger.debug('Message send complete', path);
// Only filter send returns a result
if (result) {
emitter.emit('complete', {
path : path,
recipient : recipient,
sent : result.sent,
errors : result.errors
});
} else {
emitter.emit('complete', {
path : path,
recipient : recipient
});
}
}
};
// Arguments are somewhat flexible for this method.
// path and message are fixed in the first two positions.
// After that, there may be:
// 1. options and recipient.
// 2. Just a recipient.
// The recipient may either be a Session ID (passed as a string),
// or a SendFilter.
if (options && (typeof options === 'string' || options instanceof SessionId)) {
recipient = options;
options = createSendOptions();
} else {
options = createSendOptions(options);
}
if (internal.checkConnected(emitter)) {
if (!path) {
emitter.error(new Error('Message path is null or undefined'));
return result;
}
if (message === undefined || message === null) {
emitter.error(new Error('Message content is null or undefined'));
return result;
}
var request;
if (recipient) {
var session = typeof recipient === 'string' ? parseSessionId(recipient) : recipient;
if (session) {
request = {
path : path,
content : message,
session : session,
options : options,
cid : dummyCID
};
logger.debug('Sending message to session', request);
sessionSender.send(request, sendCallback);
} else { // Try recipient as a client filter.
request = {
path : path,
content : message,
filter : recipient,
options : options,
cid : dummyCID
};
logger.debug('Sending message to filter', request);
filterSender.send(request, sendCallback);
}
} else {
request = {
path : path,
content : message,
options : options
};
logger.debug('Sending message to server', request);
sender.send(request, sendCallback);
}
}
return result;
};
this.addRequestHandler = function(path, handler, keys, requestType) {
var emitter = new Emitter();
var result = new Result(emitter);
if (internal.checkConnected(emitter)) {
try {
requireNonNull(path, "Message path");
requireNonNull(handler, "Request handler");
} catch (e) {
emitter.error(e);
return result;
}
logger.debug('Adding request handler', path);
var params = {
definition : Services.MESSAGING_RECEIVER_CLIENT,
group : ControlGroup.DEFAULT,
path : path,
keys : keys !== undefined ? keys : {}
};
var adapter = {
active : function(close, cid) {
logger.debug('Request handler active', path);
var registration = {
close: function() {
deregisterRequestHandler.send(params, function(err, response) {
if (err) {
internal.getConversationSet().discard(cid, err);
logger.debug('Error with request handler deregistration: ', err);
} else {
handler.onClose();
internal.getConversationSet().respondIfPresent(cid, response);
}
});
}
};
emitter.emit('complete', registration);
},
respond : function(pair) {
logger.debug('Request handler pair.request', path);
var request;
var context = {
sessionId: pair.request.sessionID,
path: pair.request.path,
properties: pair.request.properties
};
var requestDatatype;
try {
requestDatatype = requestType ? requestType : DataTypes.getChecked(pair.request.dataType);
request = requestDatatype.readValue(pair.request.content);
} catch (err) {
logger.info('An exception has occurred whilst reading the request:', err);
handler.onError(err);
pair.fail(err.message);
return;
}
var responder = {
respond: function(response, respType) {
var responseType = DataTypes.getChecked(respType ? respType.name() : response);
pair.callback({
responseDataType: responseType.name(),
data: responseType.writeValue(response)
});
},
reject: function(message) {
pair.fail(ErrorReason.REJECTED_REQUEST, message);
}
};
handler.onRequest(request, context, responder);
},
close : function() {
logger.debug('Request handler closed', path);
handler.onClose();
}
};
registration.registerRequestHandler(internal, params, adapter).then(null, function(err) {
emitter.error(err);
});
}
return result;
};
function sendRequestToController(emitter, path, request, reqType, respType) {
try {
requireNonNull(path, "Message path");
requireNonNull(request, "Request");
} catch (e) {
emitter.error(e);
return;
}
var requestType;
try {
requestType = reqType ? DataTypes.getChecked(reqType) : DataTypes.getByValue(request);
} catch (e) {
emitter.error(e);
return;
}
logger.debug('Sending request to server', request);
MESSAGING_SEND.send({
path : path,
type: requestType.name(),
request: requestType.writeValue(request)
}, function(err, response) {
var responseType;
if (err) {
logger.debug('Request failed', path);
emitter.error(err);
} else {
logger.debug('Request complete', path);
try {
responseType = DataTypes.getChecked(respType ? respType : response.responseDataType);
} catch (e) {
emitter.error(e);
return;
}
emitter.emit('complete', responseType.readValue(response.data));
}
});
}
function sendRequestToSession(emitter, path, request, sessionId, reqType, respType) {
try {
requireNonNull(sessionId, "Session ID");
requireNonNull(path, "Message path");
requireNonNull(request, "Request");
} catch (e) {
emitter.error(e);
return;
}
var requestType;
try {
requestType = reqType ? DataTypes.getChecked(reqType) : DataTypes.getByValue(request);
} catch (e) {
emitter.error(e);
return;
}
logger.debug('Sending request to session', sessionId, request);
MESSAGING_RECEIVER_SERVER.send({
sessionId: sessionId,
path : path,
type: requestType.name(),
request: requestType.writeValue(request)
}, function(err, response) {
var responseType;
if (err) {
logger.debug('Request failed', path);
emitter.error(err);
} else {
logger.debug('Request complete', path);
try {
responseType = DataTypes.getChecked(respType ? respType : response.responseDataType);
} catch (e) {
emitter.error(e);
return;
}
emitter.emit('complete', responseType.readValue(response.data));
}
});
}
this.sendRequest = function(
path,
request,
sessionIdOrRequestType,
requestTypeOrResponseType,
responseType) {
var emitter = new Emitter();
var result = new Result(emitter);
if (internal.checkConnected(emitter)) {
if (sessionIdOrRequestType instanceof SessionId) {
sendRequestToSession(
emitter,
path,
request,
sessionIdOrRequestType,
requestTypeOrResponseType,
responseType);
}
else if (typeof sessionIdOrRequestType === 'string') {
var sessionId = parseSessionId(sessionIdOrRequestType);
if (sessionId) {
sendRequestToSession(
emitter,
path,
request,
sessionId,
requestTypeOrResponseType,
responseType);
}
else {
sendRequestToController(
emitter,
path,
request,
sessionIdOrRequestType,
requestTypeOrResponseType);
}
}
else {
sendRequestToController(
emitter,
path,
request,
sessionIdOrRequestType,
requestTypeOrResponseType);
}
}
return result;
};
this.sendRequestToFilter = function(filter, path, request, callback, reqType, respType) {
var emitter = new Emitter();
var result = new Result(emitter);
if (internal.checkConnected(emitter)) {
try {
requireNonNull(path, "Message path");
requireNonNull(filter, "Session filter");
requireNonNull(request, "Request");
requireNonNull(callback, "Response callback");
} catch (e) {
emitter.error(e);
return result;
}
var requestType;
try {
requestType = reqType ? DataTypes.getChecked(reqType) : DataTypes.getByValue(request);
}
catch (e) {
emitter.error(e);
return;
}
var cid = internal.getConversationSet().newConversation({
onOpen : function() {
// No-op
},
onResponse : function(cid, response) {
if (response === null) {
return true;
}
var sessionID = response.sessionID;
if (response.errorReason) {
callback.onResponseError(sessionID, response.errorReason);
} else {
var responseDataType = DataTypes.getByName(response.response.responseDataType);
var responseType;
try {
responseType = DataTypes.getChecked(
respType ? respType : response.response.responseDataType);
}
catch (e) {
emitter.error(e);
return;
}
if (responseDataType.canReadAs(responseType.valueClass)) {
var value;
try {
value = responseDataType.readAs(responseType.valueClass, response.response.data);
} catch (e) {
callback.onResponseError(sessionID, e);
return false;
}
try {
callback.onResponse(sessionID, value);
} catch (e) {
logger.error("Exception within messaging filter callback", e);
}
} else {
callback.onResponseError(sessionID,
new Error("The received response was a " + responseDataType +
", which cannot be read as: " + responseType));
}
}
return false;
},
onDiscard : function(cid, err) {
callback.onError(err);
}
});
logger.debug('Sending filter request to server', request);
MESSAGING_FILTER_SEND.send({
cid : cid,
path : path,
filter : filter,
dataType : requestType,
request : requestType.writeValue(request)
}, function(err, response) {
if (err) {
logger.debug('Request failed', path);
emitter.error(err);
} else if (response.isSuccess) {
logger.debug('Request complete', path);
internal.getConversationSet().respondIfPresent(cid, null);
emitter.emit('complete', response.numberSent);
} else {
logger.debug('Request failed', path);
emitter.error(response.errors);
}
});
}
return result;
};
this.setRequestStream = function(path, stream, reqType, resType) {
var proxy = new RequestStreamProxy(
reqType ? DataTypes.getValueClassChecked(reqType) : null,
resType ? DataTypes.getValueClassChecked(resType) : null,
stream);
var old = requestStreams[path];
requestStreams[path] = proxy;
if (old) {
return old.stream;
}
};
this.removeRequestStream = function(path) {
var proxy = requestStreams[path];
delete requestStreams[path];
if (proxy) {
return proxy.stream;
}
};
});