diffusion
Version:
Diffusion JavaScript client
353 lines (352 loc) • 14.9 kB
JavaScript
"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;