UNPKG

diffusion

Version:

Diffusion JavaScript client

737 lines (617 loc) 25.5 kB
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; } }; });