diffusion
Version:
Diffusion JavaScript client
414 lines (413 loc) • 15.5 kB
JavaScript
"use strict";
/**
* @module Transport
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Transports = exports.CascadeDriver = void 0;
var errors_1 = require("./../../errors/errors");
var emitter_1 = require("./../events/emitter");
var stream_1 = require("./../events/stream");
var response_code_1 = require("./../protocol/response-code");
var parsing = require("./../transports/parsing");
var subtransports_1 = require("./../transports/subtransports");
var ws_1 = require("./../transports/ws");
var xhr_1 = require("./../transports/xhr");
var logger_1 = require("./../util/logger");
var math_1 = require("../util/math");
var log = logger_1.create('Cascading');
/**
* The known transports. Will be looked up by name.
*/
var knownTransports = Object.freeze({
WS: ws_1.WSTransport,
XHR: xhr_1.XHRTransport,
WEBSOCKET: ws_1.WSTransport,
HTTP_POLLING: xhr_1.XHRTransport
});
/**
* Filter a list of transport names, removing any unknown or disabled transports.
*
* @param requestedTransports array of transport names
* @param subtransports the available subtransports
* @return the filtered array of transport names
*/
function filterTransports(requestedTransports, subtransports) {
return requestedTransports.filter(function (name) {
// eslint-disable-next-line no-prototype-builtins
return name && knownTransports.hasOwnProperty(name) && subtransports[name].enabled;
});
}
/**
* Check if a response code indicates that the client should attempt to cascade
*
* @param response the response code
* @returns `true` if the client should attempt to cascade on receiving a response
*/
function isCascadableResponse(response) {
return response === response_code_1.responseCodes.ERROR;
}
/**
* Create the selected transport if enabled.
*
* @param subtransports the subtransports that can be made
* @param options the options used to connect
* @param 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.factory, subtransport.allowsOptions);
}
else {
// any unknown transports should have been filtered out by the
// filterTransports function
throw new errors_1.InternalError('Unknown transport name: ' + name);
}
}
/**
* Encapsulate the set of available transport mechanisms and expose them in an
* implementation independent manner.
*/
var CascadeDriver = /** @class */ (function (_super) {
__extends(CascadeDriver, _super);
/**
* Create a new CascadeDriver instance
*
* @param subtransports the available subtransports
* @param opts the connection options
* @param transports a list of available transport names
* @param cascadeNotifications an emitter for emitting events on the {@link Transports} stream
*/
function CascadeDriver(subtransports, opts, transports, cascadeNotifications) {
var _this = this;
var factory = emitter_1.Emitter.create();
_this = _super.call(this, factory) || this;
_this.emitter = factory.emitter(_this);
_this.transports = transports;
_this.remainingTransports = __spreadArray([], __read(transports));
_this.cascadeNotifications = cascadeNotifications;
_this.name = null;
_this.req = null;
_this.responseReceived = false;
_this.transportFactory = createTransport.bind(null, subtransports, opts);
_this.transport = _this.selectTransport();
// bind this to the event handlers so that we can pass them to the stream's
// on and off functions
_this.onData = _this.onData.bind(_this);
_this.onClose = _this.onClose.bind(_this);
_this.onError = _this.onError.bind(_this);
_this.onConnectionResponse = _this.onConnectionResponse.bind(_this);
_this.onParsingError = _this.onParsingError.bind(_this);
_this.retryStrategy = __assign({}, opts.retry);
return _this;
}
/**
* Called when the transport emits a `data` event.
*
* Forwards the data and emits a `data` on this stream
*
* @param data the message data
*/
CascadeDriver.prototype.onData = function (data) {
this.emitter.immediate('data', data);
};
/**
* Called when the transport emits a `close` event.
*
* Only emits a `close` event on this stream if it can't cascade further
*
* @param reason the reason for closing
*/
CascadeDriver.prototype.onClose = function (reason) {
if (!this.responseReceived && this.cascade(true)) {
// attempting to cascade
return;
}
// transport closed without receiving any response
// close event suppressed to prevent reentering this handler
this.emitter.emit('close', reason);
};
/**
* Called when the transport emits an `error` event.
*
* Forwards the data and emits a `error` on this stream
*
* @param error the error that occurred
*/
CascadeDriver.prototype.onError = function (error) {
this.emitter.emit('error', error);
};
/**
* Called when the connection response is received.
*
* @param response the connection response from the server
*/
CascadeDriver.prototype.onConnectionResponse = function (response) {
this.responseReceived = true;
if (isCascadableResponse(response.response)) {
if (this.cascade()) {
// attempting to cascade
return null;
}
// transport closed and close event emitted
}
return this.onHandshakeSuccess(response);
};
/**
* Called when the connection response cannot be parsed.
*
* @param error the error that occurred
*/
CascadeDriver.prototype.onParsingError = function (error) {
this.responseReceived = true;
if (!this.cascade()) {
// transports exhausted
this.onHandshakeError(error);
}
};
/**
* Close the current transport quietly, without generating any events.
*/
CascadeDriver.prototype.closeQuietly = function () {
if (this.transport) {
this.transport.off('data', this.onData);
this.transport.off('close', this.onClose);
this.transport.off('error', this.onError);
this.transport.close();
this.transport = null;
}
};
/**
* Select and create the next enabled transport.
*
* If there are no more transports then a `close` event may be emitted on
* this stream and a `transports-exhausted` event is emitted on the {@link
* Transports} stream.
*
* @param suppressClose a flag indicating whether a `close` event should be
* suppressed
* @return the selected transport
*/
CascadeDriver.prototype.selectTransport = function (suppressClose) {
if (suppressClose === void 0) { suppressClose = false; }
if (this.remainingTransports.length === 0) {
this.name = null;
this.transport = null;
log.debug('Transports exhausted');
if (this.retryStrategy.attempts <= 0) {
this.cascadeNotifications.emit('transports-exhausted');
if (!suppressClose) {
this.emitter.emit('close');
}
}
return null;
}
this.name = this.remainingTransports.shift();
this.transport = this.transportFactory(this.name);
log.debug('Selecting transport', this.name);
this.cascadeNotifications.emit('transport-selected', this.name);
return this.transport;
};
/**
* Connect to the current transport
*
* A `cascading-connect` event is emitted on the {@link Transports} stream.
*
* @returns true if attempting to connect
*/
CascadeDriver.prototype.internalConnect = function () {
if (this.transport === null) {
throw new errors_1.InternalError('Unable to connect when no transport is set');
}
this.responseReceived = false;
log.debug('Attempting to connect');
this.cascadeNotifications.emit('cascading-connect');
this.transport.on('data', this.onData);
this.transport.on('close', this.onClose);
this.transport.on('error', this.onError);
try {
this.transport.connect(this.req, parsing.connectionResponse.bind(null, this.onConnectionResponse, this.onParsingError));
}
catch (e) {
if (!this.cascade()) {
// transports exhausted
throw e;
}
}
return true;
};
/**
* Cascade to the next available transport
*
* A `close` event may be emitted on this stream and a `cascade` event is
* emitted on the {@link Transports} stream.
*
* @param suppressClose a flag indicating whether a `close` event should be
* suppressed
* @returns true if attempting to connect, false if transports exhausted
*/
CascadeDriver.prototype.cascade = function (suppressClose) {
var _this = this;
this.closeQuietly();
if (!this.selectTransport(suppressClose)) {
if (this.retryStrategy.attempts !== math_1.MAX_SAFE_INTEGER &&
--this.retryStrategy.attempts < 0) {
return false;
}
this.remainingTransports = __spreadArray([], __read(this.transports));
if (this.selectTransport(suppressClose)) {
setTimeout(function () {
_this.cascadeNotifications.emit('cascade');
_this.internalConnect();
}, this.retryStrategy.interval);
return true;
}
else {
return false;
}
}
this.cascadeNotifications.emit('cascade');
return this.internalConnect();
};
/**
* Connect using the available transports
*
* @param request the connection request.
* @param handshake the handshake handler
* @param handshakeError the handshake error handler
*/
CascadeDriver.prototype.connect = function (request, handshake, handshakeError) {
this.req = request;
this.onHandshakeSuccess = handshake;
this.onHandshakeError = handshakeError;
this.internalConnect();
};
/**
* Send a binary message to the server.
*
* @param message the message to send
*/
CascadeDriver.prototype.dispatch = function (message) {
if (this.transport === null) {
throw new errors_1.InternalError('Unable to send message when no transport is set');
}
this.transport.dispatch(message);
};
/**
* Close the transport
*
* @return this stream
*/
CascadeDriver.prototype.close = function () {
if (this.transport !== null) {
this.responseReceived = true; // to prevent cascading on connection timeout
// a close event will be emitted by the transport after the close is complete
this.transport.close();
this.transport = null;
}
return this;
};
return CascadeDriver;
}(stream_1.StreamImpl));
exports.CascadeDriver = CascadeDriver;
/**
* Encapsulate the set of available transport mechanisms and expose them in an
* implementation independent manner.
*/
var Transports = /** @class */ (function (_super) {
__extends(Transports, _super);
/**
* Create a new Transports instance
*
* @param subtransports the available subtransports
*/
function Transports(subtransports) {
var _this = this;
var factory = emitter_1.Emitter.create();
_this = _super.call(this, factory) || this;
_this.emitter = factory.emitter(_this);
_this.subtransports = subtransports;
return _this;
}
/**
* Create a new Transports object.
*
* @param subtransports allows specifying the available subtransports for
* testing. If not specified, the {@link
* Subtransports} * will be used.
*/
Transports.create = function (subtransports) {
if (subtransports) {
return new Transports(subtransports);
}
else {
return new Transports(subtransports_1.Subtransports);
}
};
/**
* Get a new {@link CascadeDriver} with a given set of options
*
* @param options the options from which to create a driver.
* @return the cascade driver if the `options` contain valid
* transports, otherwise `null`
*/
Transports.prototype.get = function (options) {
var validTransports = filterTransports(options.transports, this.subtransports);
if (validTransports.length === 0) {
log.warn('No valid transports found');
return null;
}
return new CascadeDriver(this.subtransports, options, validTransports, this.emitter);
};
return Transports;
}(stream_1.StreamImpl));
exports.Transports = Transports;