diffusion
Version:
Diffusion JavaScript client
281 lines (243 loc) • 7.89 kB
JavaScript
/*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;