UNPKG

diffusion

Version:

Diffusion JavaScript client

353 lines (352 loc) 14.9 kB
"use strict"; /** * @module V4Stack */ 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 __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Connection = void 0; var errors_1 = require("./../../errors/errors"); var emitter_1 = require("./../events/emitter"); var stream_1 = require("./../events/stream"); var buffer_output_stream_1 = require("./../io/buffer-output-stream"); var recovery_buffer_1 = require("./../message-queue/recovery-buffer"); var connection_response_deserialiser_1 = require("./../protocol/connection-response-deserialiser"); var response_code_1 = require("./../protocol/response-code"); var fsm_1 = require("./../util/fsm"); var Logger = require("./../util/logger"); var Message = require("./../v4-stack/message"); var close_reason_1 = require("../../client/close-reason"); var logger = Logger.create('Connection'); /** * The amount of time to wait for the server to close the connection after a req is sent. */ var CLOSE_TIMEOUT = global.DIFFUSION_CLOSE_TIMEOUT || 1000; /** * The default recovery buffer index size */ var RECOVERY_BUFFER_INDEX_SIZE = global.DIFFUSION_RECOVERY_BUFFER_INDEX_SIZE || 8; /** * The layer that abstracts across transports and handles the connection-level * protocol. The externally exposed events from a connection are `connect`, * `disconnect` and `error`. A connection only cares about whether it is connected or not. * * Higher level semantics such as reconnect or session close, are determined by * the consumer of this connection's events, i.e the internal session. * * The following events are emitted by the connection are * * Event | Argument * ------------ | ------------ * `connect` | the handshake response * `data` | {@link Message} * `disconnect` | {@link CloseReason} * `close` | {@link CloseReason} * `error` | Error * * TODO FB19940 update documentation to reflect the type of the handshake response */ var Connection = /** @class */ (function (_super) { __extends(Connection, _super); /** * Create a new connection * * @param transports the transports that can be used for the connecttion * @param reconnectTimeout a reconnection timeout * @param recoveryBufferSize size of the recovery buffer */ function Connection(transports, reconnectTimeout, recoveryBufferSize) { var _this = this; var factory = emitter_1.Emitter.create(); _this = _super.call(this, factory) || this; _this.emitter = factory.emitter(_this); _this.fsm = new fsm_1.FSM('disconnected', { connecting: ['connected', 'disconnected', 'closed'], connected: ['disconnecting', 'disconnected', 'closed'], disconnecting: ['disconnected'], disconnected: ['connecting', 'closed'], closed: [] }); _this.transports = transports; _this.reconnectTimeout = reconnectTimeout; _this.lastSentSequence = 0; _this.lastReceivedSequence = 0; _this.recoveryBuffer = new recovery_buffer_1.RecoveryBuffer(recoveryBufferSize, RECOVERY_BUFFER_INDEX_SIZE); _this.fsm.on('change', function (previous, current) { logger.debug("State changed: " + previous + " -> " + current); }); return _this; } /** * Parse {@link Message} from data and emit the `data` event. * * If the message could not be parsed, the connection will be cloed with * {@link CloseReasonEnum.CONNECTION_ERROR} * * @param data the raw message data */ Connection.prototype.onData = function (data) { var _this = this; logger.debug('Received message from transport', data); Message.parse(data, function (err, message) { if (!err && message) { if (message.type === Message.types.ABORT_NOTIFICATION) { if (_this.fsm.state !== 'disconnecting') { _this.closeReason = close_reason_1.CloseReasonEnum.CLOSED_BY_SERVER; } _this.transport.close(); return; } _this.lastReceivedSequence++; _this.emitter.emit('data', message); } else { logger.warn('Unable to parse message', err); if (_this.fsm.change('closed')) { // should this be TRANSPORT_ERROR? _this.closeReason = close_reason_1.CloseReasonEnum.CONNECTION_ERROR; _this.transport.close(); } } }); }; /** * Close the connection and emit a `close` event. If the connection was in * a `disconnected` state, then a `disconnect` event is emitted instead. */ Connection.prototype.onClose = function () { if (this.fsm.change('disconnected') || this.fsm.change('closed')) { clearInterval(this.scheduledRecoveryBufferTrim); clearTimeout(this.scheduledClose); logger.trace('Transport closed: ', this.closeReason); if (this.fsm.state === 'disconnected') { this.emitter.emit('disconnect', this.closeReason); } else { this.emitter.close(this.closeReason); } } else { logger.debug('Unable to disconnect, current state: ', this.fsm.state); } }; /** * Handle an error * * Closes the connection with {@link CloseReasonEnum.TRANSPORT_ERROR} * * @param err the error that occured */ Connection.prototype.onError = function (err) { logger.error('Error from transport', err); if (this.fsm.change('closed')) { this.closeReason = close_reason_1.CloseReasonEnum.TRANSPORT_ERROR; this.transport.close(); } }; /** * Handle connection responses. * * @param response parsed connection response */ Connection.prototype.onHandshakeSuccess = function (response) { if (response.response === response_code_1.responseCodes.RECONNECTED) { var messageDelta = this.lastSentSequence - response.sequence + 1; if (this.recoveryBuffer.recover(messageDelta, this.dispatch.bind(this))) { this.recoveryBuffer.clear(); this.lastSentSequence = response.sequence - 1; } else { var outboundLoss = this.lastSentSequence - this.recoveryBuffer.size() - response.sequence + 1; logger.warn("Unable to reconnect due to lost messages (" + outboundLoss + ")"); if (this.fsm.change('disconnected')) { this.closeReason = close_reason_1.CloseReasonEnum.LOST_MESSAGES; this.transport.close(); } return response; } } if (response.success && this.fsm.change('connected')) { logger.trace('Connection response: ', response.response); this.closeReason = close_reason_1.CloseReasonEnum.TRANSPORT_ERROR; this.emitter.emit('connect', response); } else { logger.error('Connection failed: ', response.response); switch (response.response) { case response_code_1.responseCodes.AUTHENTICATION_FAILED: this.closeReason = close_reason_1.CloseReasonEnum.ACCESS_DENIED; break; case response_code_1.responseCodes.LICENSE_EXCEEDED: this.closeReason = close_reason_1.CloseReasonEnum.LICENSE_EXCEEDED; break; case response_code_1.responseCodes.DOWNGRADE: this.closeReason = close_reason_1.CloseReasonEnum.PROTOCOL_VERSION_MISMATCH; break; default: this.closeReason = close_reason_1.CloseReasonEnum.HANDSHAKE_REJECTED; break; } this.transport.close(); } clearTimeout(this.scheduledClose); return response; }; /** * Handle connection response parsing errors. * * @param error error thrown parsing the response */ Connection.prototype.onHandshakeError = function (error) { if (error.reason === connection_response_deserialiser_1.DeserialisationErrorReason.PROTOCOL_VERSION_MISMATCH) { this.closeReason = close_reason_1.CloseReasonEnum.PROTOCOL_VERSION_MISMATCH; } else { this.closeReason = close_reason_1.CloseReasonEnum.HANDSHAKE_ERROR; } // transport closed and close event emitted clearTimeout(this.scheduledClose); logger.trace('Unable to deserialise handshake response', error); return; }; /** * Establish a connection with a provided connection request and connection options. * * @param request the connection request * @param opts options passed to the transports * @param timeout a timeout after which the connection will be closed * @throws an {@link IllegalArgumentError} if no valid transports are requested */ Connection.prototype.connect = function (request, opts, timeout) { var _this = this; if (this.fsm.state !== 'disconnected') { logger.warn('Cannot connect, current state: ', this.fsm.state); return; } if (this.fsm.change('connecting')) { this.closeReason = close_reason_1.CloseReasonEnum.CONNECTION_ERROR; // begin a connection cycle with a transport this.transport = this.transports.get(opts); /* tslint:disable-next-line:strict-type-predicates */ if (this.transport === null) { throw new errors_1.IllegalArgumentError('No valid transports requested'); } this.transport.on('data', this.onData.bind(this)); this.transport.on('close', this.onClose.bind(this)); this.transport.on('error', this.onError.bind(this)); // attempt to connect the transport this.transport.connect(request, this.onHandshakeSuccess.bind(this), this.onHandshakeError.bind(this)); // ensure we will emit a close reason if we're unable to connect within a timeout this.scheduledClose = setTimeout(function () { logger.debug("Timed out connection attempt after " + timeout); _this.closeReason = close_reason_1.CloseReasonEnum.CONNECTION_TIMEOUT; _this.transport.close(); }, timeout); this.scheduledRecoveryBufferTrim = setInterval(function () { if (_this.fsm.state === 'connected') { _this.recoveryBuffer.flush(Date.now() - _this.reconnectTimeout); } }, this.reconnectTimeout); } }; /** * Reset the recovery buffer and message sequence numbers */ Connection.prototype.resetSequences = function () { this.recoveryBuffer.clear(); this.lastSentSequence = 0; this.lastReceivedSequence = 0; }; /** * Get the sequence number of the oldest message in the recovery buffer * * @return the oldest sequence number */ Connection.prototype.getAvailableSequence = function () { return this.lastSentSequence + 1 - this.recoveryBuffer.size(); }; /** * Internal dispatch method for converting a message into a buffer and * dispatching it through the active transport. * * The message is added to the recovery buffer * * @param message the message that should be dispatched */ Connection.prototype.dispatch = function (message) { var bos = new buffer_output_stream_1.BufferOutputStream(); Message.writeToBuffer(message, bos); this.lastSentSequence += 1; this.recoveryBuffer.put(message); this.recoveryBuffer.markTime(Date.now()); this.transport.dispatch(bos.getBuffer()); }; /** * Send a message. * * Sending messages is only valid in a 'connected' state. This method will * return false otherwise. * * @param message the message that should be dispatched * @return `true` if the message was succesfully enqueued * on the transport. */ Connection.prototype.send = function (message) { if (this.fsm.state === 'connected') { this.dispatch(message); return true; } return false; }; /** * Close the connection. When the connection has been closed, it will emit a * `disconnected` event. * * @param reason the close reason */ Connection.prototype.close = function (reason) { this.closeReason = reason; if (this.fsm.state === 'disconnected') { // transport is already closed and the event isn't emitted again so the callback is called directly this.onClose(); } else if (this.fsm.change('disconnecting')) { // we allow the server a grace period to close the transport connection this.dispatch(Message.create(Message.types.CLOSE_REQUEST)); // explicitly close the transport if a given timeout elapses. this.scheduledClose = setTimeout(this.transport.close.bind(this.transport), CLOSE_TIMEOUT); } return this; }; /** * Close the connection when it is idling */ Connection.prototype.closeIdleConnection = function () { if (this.fsm.change('disconnecting')) { logger.debug('Connection detected as idle'); this.closeReason = close_reason_1.CloseReasonEnum.IDLE_CONNECTION; this.transport.close(); } }; /** * Get the current state of the connection. */ Connection.prototype.getState = function () { return this.fsm.state; }; return Connection; }(stream_1.StreamImpl)); exports.Connection = Connection;