UNPKG

diffusion

Version:

Diffusion JavaScript client

414 lines (413 loc) 15.5 kB
"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;