UNPKG

diffusion

Version:

Diffusion JavaScript client

281 lines (243 loc) 7.89 kB
/*eslint valid-jsdoc: "off"*/ var WSTransport = require('transports/ws'); var XHRTransport = require('transports/xhr'); var Emitter = require('events/emitter'); var ResponseCode = require('protocol/response-code'); var standardSubtransports = require('transports/subtransports'); var parsing = require('transports/parsing'); var logger = require('util/logger').create('Cascading'); /** * The known transports. Will be looked up by name. */ var knownTransports = { WS : WSTransport, XHR : XHRTransport, WEBSOCKET : WSTransport, HTTP_POLLING : XHRTransport }; /** * Filter a list of transport names of any unknown or disabled transports. * @param {Array} requestedTransports - Array of transport names * @param {Subtransports} subtransports - The available subtransports * @returns {Array} - Filtered array of transport names */ function filterTransports(requestedTransports, subtransports) { return requestedTransports.filter(function(name) { return name && knownTransports.hasOwnProperty(name); }).filter(function(name) { return subtransports[name].enabled; }); } /** * @returns {boolean} If the client should attempt to cascade on receiving a response */ function isCascadableResponse(response) { return response === ResponseCode.ERROR; } /** * Create the selected transport if enabled. * @param {Subtransports} subtransports - The subtransports that can be made * @param {SessionOptions} options - The options used to connect * @param {String} name - The name of the subtransport to use * @returns {*} an available transport if enabled */ function createTransport(subtransports, options, name) { var transport = knownTransports[name]; var subtransport = subtransports[name]; if (transport && subtransport && subtransport.enabled) { return new transport(options, subtransport.constructor); } else { // Any unknown transports should have been filtered out by the // filterTransports function throw new Error('Unknown transport name: ' + name); } } /** * Encapsulate the set of available transport mechanisms and expose them in an * implementation independent manner. */ function CascadeDriver(subtransports, opts, transports, cascadeNotifications) { var emitter = Emitter.assign(this); var self = this; /** * The name of the currently selected transport. */ this.name = null; /** * The connection request. */ var req = null; /** * The handshake. */ var onHandshakeSuccess = null; var onHandshakeError = null; var responseReceived = false; var transportFactory = createTransport.bind(null, subtransports, opts); /** * The current transport object. */ var transport = selectTransport(); /** * Called when the transport emits a data event. */ function onData(data) { emitter.emit('data', data); } /** * Called when the transport emits a close event. */ function onClose(reason) { if (!responseReceived && cascade(true)) { // Attempting to cascade return; } // Transport closed without receiving any response // close event suppressed to prevent reentering this handler emitter.emit('close', reason); } /** * Called when the transport emits an error event. */ function onError(error) { emitter.emit('error', error); } /** * Called when the connection response is received. */ function onConnectionResponse(response) { responseReceived = true; if (isCascadableResponse(response.response)) { if (cascade()) { // Attempting to cascade return; } // Transport closed and close event emitted } return onHandshakeSuccess(response); } /** * Called when the connection response cannot be parsed. */ function onParsingError(error) { responseReceived = true; if (!cascade()) { // Transports exhausted return onHandshakeError(error); } } /** * Close the current transport quietly, without generating any events. */ function closeQuietly() { if (transport) { transport.off('data', onData); transport.off('close', onClose); transport.off('error', onError); transport.close(); transport = null; } } /** * Select and create the next enabled transport. * @returns the selected transport */ function selectTransport(suppressClose) { if (transports.length === 0) { self.name = null; transport = null; logger.debug('Transports exhausted'); cascadeNotifications.emit('transports-exhausted'); if (!suppressClose) { emitter.emit('close'); } return null; } self.name = transports.shift(); transport = transportFactory(self.name); logger.debug('Selecting transport', self.name); cascadeNotifications.emit('transport-selected', self.name); return transport; } /** * @returns true if attempting to connect */ function internalConnect() { responseReceived = false; logger.debug('Attempting to connect'); cascadeNotifications.emit('cascading-connect'); transport.on('data', onData); transport.on('close', onClose); transport.on('error', onError); try { transport.connect( req, parsing.connectionResponse.bind(null, onConnectionResponse, onParsingError) ); } catch(e) { if (!cascade()) { // Transports exhausted throw e; } } return true; } /** * @returns true if attempting to connect, false if transports exhausted */ function cascade(suppressClose) { closeQuietly(); if (!selectTransport(suppressClose)) { return false; } cascadeNotifications.emit('cascade'); return internalConnect(); } this.connect = function connect(request, handshake, handshakeError) { req = request; onHandshakeSuccess = handshake; onHandshakeError = handshakeError; internalConnect(); }; this.dispatch = function dispatch(message) { if (transport === null) { throw new Error('Unable to send message when no transport is set'); } transport.dispatch(message); }; this.close = function close() { if (transport !== null) { responseReceived = true; // To prevent cascading on connection timeout // A close event will be emitted by the transport after the close is complete transport.close(); transport = null; } }; } /** * Encapsulate the set of available transport mechanisms and expose them in an * implementation independent manner. */ function Transports(subtransports) { var emitter = Emitter.assign(this); this.get = function get(options) { var validTransports = filterTransports(options.transports, subtransports); if (validTransports.length === 0) { logger.warn('No valid transports found'); return null; } return new CascadeDriver(subtransports, options, validTransports, emitter); }; } /** * Create a new transports object. */ Transports.create = function(subtransports) { if (subtransports) { return new Transports(subtransports); } else { return new Transports(standardSubtransports); } }; module.exports = Transports;