@deepstream/client
Version:
the javascript client for deepstreamIO
515 lines (514 loc) • 24.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Connection = void 0;
var constants_1 = require("../constants");
var message_parser_1 = require("@deepstream/protobuf/dist/src/message-parser");
var state_machine_1 = require("../util/state-machine");
var utils = require("../util/utils");
var emitter_1 = require("../util/emitter");
var pkg = require("../../package.json");
var Connection = /** @class */ (function () {
function Connection(services, options, url, emitter) {
var _this = this;
this.services = services;
this.options = options;
this.reconnectTimeout = null;
this.authParams = null;
this.handlers = new Map();
this.authCallback = null;
this.resumeCallback = null;
this.emitter = emitter;
this.internalEmitter = new emitter_1.Emitter();
this.isInLimbo = true;
this.clientData = null;
this.heartbeatIntervalTimeout = null;
this.endpoint = null;
this.reconnectionAttempt = 0;
this.limboTimeout = null;
var isReconnecting = false;
var firstOpen = true;
this.stateMachine = new state_machine_1.StateMachine(this.services.logger, {
init: constants_1.CONNECTION_STATE.CLOSED,
onStateChanged: function (newState, oldState) {
if (newState === oldState) {
return;
}
emitter.emit(constants_1.EVENT.CONNECTION_STATE_CHANGED, newState);
if (newState === constants_1.CONNECTION_STATE.RECONNECTING) {
_this.isInLimbo = true;
isReconnecting = true;
if (oldState !== constants_1.CONNECTION_STATE.CLOSED) {
_this.internalEmitter.emit(constants_1.EVENT.CONNECTION_LOST);
_this.limboTimeout = _this.services.timerRegistry.add({
duration: _this.options.offlineBufferTimeout,
context: _this,
callback: function () {
_this.isInLimbo = false;
_this.internalEmitter.emit(constants_1.EVENT.EXIT_LIMBO);
}
});
}
}
else if (newState === constants_1.CONNECTION_STATE.OPEN && (isReconnecting || firstOpen)) {
firstOpen = false;
_this.isInLimbo = false;
_this.internalEmitter.emit(constants_1.EVENT.CONNECTION_REESTABLISHED);
_this.services.timerRegistry.remove(_this.limboTimeout);
}
},
transitions: [
{ name: "initialised" /* TRANSITIONS.INITIALISED */, from: constants_1.CONNECTION_STATE.CLOSED, to: constants_1.CONNECTION_STATE.INITIALISING },
{ name: "connected" /* TRANSITIONS.CONNECTED */, from: constants_1.CONNECTION_STATE.INITIALISING, to: constants_1.CONNECTION_STATE.AWAITING_CONNECTION },
{ name: "connected" /* TRANSITIONS.CONNECTED */, from: constants_1.CONNECTION_STATE.REDIRECTING, to: constants_1.CONNECTION_STATE.AWAITING_CONNECTION },
{ name: "connected" /* TRANSITIONS.CONNECTED */, from: constants_1.CONNECTION_STATE.RECONNECTING, to: constants_1.CONNECTION_STATE.AWAITING_CONNECTION },
{ name: "challenge" /* TRANSITIONS.CHALLENGE */, from: constants_1.CONNECTION_STATE.AWAITING_CONNECTION, to: constants_1.CONNECTION_STATE.CHALLENGING },
{ name: "redirected" /* TRANSITIONS.CONNECTION_REDIRECTED */, from: constants_1.CONNECTION_STATE.CHALLENGING, to: constants_1.CONNECTION_STATE.REDIRECTING },
{ name: "challenge-denied" /* TRANSITIONS.CHALLENGE_DENIED */, from: constants_1.CONNECTION_STATE.CHALLENGING, to: constants_1.CONNECTION_STATE.CHALLENGE_DENIED },
{ name: "accepted" /* TRANSITIONS.CHALLENGE_ACCEPTED */, from: constants_1.CONNECTION_STATE.CHALLENGING, to: constants_1.CONNECTION_STATE.AWAITING_AUTHENTICATION, handler: this.onAwaitingAuthentication.bind(this) },
{ name: "authentication-timeout" /* TRANSITIONS.AUTHENTICATION_TIMEOUT */, from: constants_1.CONNECTION_STATE.AWAITING_CONNECTION, to: constants_1.CONNECTION_STATE.AUTHENTICATION_TIMEOUT },
{ name: "authentication-timeout" /* TRANSITIONS.AUTHENTICATION_TIMEOUT */, from: constants_1.CONNECTION_STATE.AWAITING_AUTHENTICATION, to: constants_1.CONNECTION_STATE.AUTHENTICATION_TIMEOUT },
{ name: "authenticate" /* TRANSITIONS.AUTHENTICATE */, from: constants_1.CONNECTION_STATE.AWAITING_AUTHENTICATION, to: constants_1.CONNECTION_STATE.AUTHENTICATING },
{ name: "unsuccesful-login" /* TRANSITIONS.UNSUCCESFUL_LOGIN */, from: constants_1.CONNECTION_STATE.AUTHENTICATING, to: constants_1.CONNECTION_STATE.AWAITING_AUTHENTICATION },
{ name: "succesful-login" /* TRANSITIONS.SUCCESFUL_LOGIN */, from: constants_1.CONNECTION_STATE.AUTHENTICATING, to: constants_1.CONNECTION_STATE.OPEN },
{ name: "too-many-auth-attempts" /* TRANSITIONS.TOO_MANY_AUTH_ATTEMPTS */, from: constants_1.CONNECTION_STATE.AUTHENTICATING, to: constants_1.CONNECTION_STATE.TOO_MANY_AUTH_ATTEMPTS },
{ name: "too-many-auth-attempts" /* TRANSITIONS.TOO_MANY_AUTH_ATTEMPTS */, from: constants_1.CONNECTION_STATE.AWAITING_AUTHENTICATION, to: constants_1.CONNECTION_STATE.TOO_MANY_AUTH_ATTEMPTS },
{ name: "authentication-timeout" /* TRANSITIONS.AUTHENTICATION_TIMEOUT */, from: constants_1.CONNECTION_STATE.AWAITING_AUTHENTICATION, to: constants_1.CONNECTION_STATE.AUTHENTICATION_TIMEOUT },
{ name: "reconnect" /* TRANSITIONS.RECONNECT */, from: constants_1.CONNECTION_STATE.RECONNECTING, to: constants_1.CONNECTION_STATE.RECONNECTING },
{ name: "closed" /* TRANSITIONS.CLOSED */, from: constants_1.CONNECTION_STATE.CLOSING, to: constants_1.CONNECTION_STATE.CLOSED },
{ name: "offline" /* TRANSITIONS.OFFLINE */, from: constants_1.CONNECTION_STATE.PAUSING, to: constants_1.CONNECTION_STATE.OFFLINE },
{ name: "error" /* TRANSITIONS.ERROR */, to: constants_1.CONNECTION_STATE.RECONNECTING },
{ name: "connection-lost" /* TRANSITIONS.LOST */, to: constants_1.CONNECTION_STATE.RECONNECTING },
{ name: "resume" /* TRANSITIONS.RESUME */, to: constants_1.CONNECTION_STATE.RECONNECTING },
{ name: "pause" /* TRANSITIONS.PAUSE */, to: constants_1.CONNECTION_STATE.PAUSING },
{ name: "close" /* TRANSITIONS.CLOSE */, to: constants_1.CONNECTION_STATE.CLOSING },
]
});
this.stateMachine.transition("initialised" /* TRANSITIONS.INITIALISED */);
this.originalUrl = utils.parseUrl(url, this.options.path);
this.url = this.originalUrl;
if (!options.lazyConnect) {
this.createEndpoint();
}
}
Object.defineProperty(Connection.prototype, "isConnected", {
get: function () {
return this.stateMachine.state === constants_1.CONNECTION_STATE.OPEN;
},
enumerable: false,
configurable: true
});
Connection.prototype.onLost = function (callback) {
this.internalEmitter.on(constants_1.EVENT.CONNECTION_LOST, callback);
};
Connection.prototype.removeOnLost = function (callback) {
this.internalEmitter.off(constants_1.EVENT.CONNECTION_LOST, callback);
};
Connection.prototype.onReestablished = function (callback) {
this.internalEmitter.on(constants_1.EVENT.CONNECTION_REESTABLISHED, callback);
};
Connection.prototype.removeOnReestablished = function (callback) {
this.internalEmitter.off(constants_1.EVENT.CONNECTION_REESTABLISHED, callback);
};
Connection.prototype.onExitLimbo = function (callback) {
this.internalEmitter.on(constants_1.EVENT.EXIT_LIMBO, callback);
};
Connection.prototype.registerHandler = function (topic, callback) {
this.handlers.set(topic, callback);
};
Connection.prototype.sendMessage = function (message) {
if (!this.isOpen()) {
this.services.logger.error(message, constants_1.EVENT.IS_CLOSED);
return;
}
if (this.endpoint) {
this.endpoint.sendParsedMessage(message);
}
};
Connection.prototype.authenticate = function (authParamsOrCallback, callback) {
if (authParamsOrCallback &&
typeof authParamsOrCallback !== 'object' &&
typeof authParamsOrCallback !== 'function') {
throw new Error('invalid argument authParamsOrCallback');
}
if (callback && typeof callback !== 'function') {
throw new Error('invalid argument callback');
}
if (this.stateMachine.state === constants_1.CONNECTION_STATE.CHALLENGE_DENIED ||
this.stateMachine.state === constants_1.CONNECTION_STATE.TOO_MANY_AUTH_ATTEMPTS ||
this.stateMachine.state === constants_1.CONNECTION_STATE.AUTHENTICATION_TIMEOUT) {
this.services.logger.error({ topic: constants_1.TOPIC.CONNECTION }, constants_1.EVENT.IS_CLOSED);
return;
}
if (authParamsOrCallback) {
// @ts-ignore
this.authParams = typeof authParamsOrCallback === 'object' ? authParamsOrCallback : {};
}
if (authParamsOrCallback && typeof authParamsOrCallback === 'function') {
this.authCallback = authParamsOrCallback;
}
else if (callback) {
this.authCallback = callback;
}
else {
this.authCallback = function () { };
}
// if (this.stateMachine.state === CONNECTION_STATE.CLOSED && !this.endpoint) {
// this.createEndpoint()
// return
// }
if (this.stateMachine.state === constants_1.CONNECTION_STATE.AWAITING_AUTHENTICATION && this.authParams) {
this.sendAuthParams();
}
if (!this.endpoint) {
this.createEndpoint();
}
};
/*
* Returns the current connection state.
*/
Connection.prototype.getConnectionState = function () {
return this.stateMachine.state;
};
Connection.prototype.isOpen = function () {
var connState = this.getConnectionState();
return connState !== constants_1.CONNECTION_STATE.CLOSED
&& connState !== constants_1.CONNECTION_STATE.ERROR
&& connState !== constants_1.CONNECTION_STATE.CLOSING;
};
/**
* Closes the connection. Using this method
* will prevent the client from reconnecting.
*/
Connection.prototype.close = function () {
this.services.timerRegistry.remove(this.heartbeatIntervalTimeout);
this.sendMessage({
topic: constants_1.TOPIC.CONNECTION,
action: constants_1.CONNECTION_ACTION.CLOSING
});
this.stateMachine.transition("close" /* TRANSITIONS.CLOSE */);
};
Connection.prototype.pause = function () {
this.stateMachine.transition("pause" /* TRANSITIONS.PAUSE */);
this.services.timerRegistry.remove(this.heartbeatIntervalTimeout);
if (this.endpoint) {
this.endpoint.close();
}
};
Connection.prototype.resume = function (callback) {
this.stateMachine.transition("resume" /* TRANSITIONS.RESUME */);
this.resumeCallback = callback;
this.tryReconnect();
};
/**
* Creates the endpoint to connect to using the url deepstream
* was initialised with.
*/
Connection.prototype.createEndpoint = function () {
this.endpoint = this.services.socketFactory(this.url, this.options.socketOptions, this.options.heartbeatInterval);
this.endpoint.onopened = this.onOpen.bind(this);
this.endpoint.onerror = this.onError.bind(this);
this.endpoint.onclosed = this.onClose.bind(this);
this.endpoint.onparsedmessages = this.onMessages.bind(this);
};
/********************************
****** Endpoint Callbacks ******
/********************************/
/**
* Will be invoked once the connection is established. The client
* can't send messages yet, and needs to get a connection ACK or REDIRECT
* from the server before authenticating
*/
Connection.prototype.onOpen = function () {
this.clearReconnect();
this.checkHeartBeat();
this.stateMachine.transition("connected" /* TRANSITIONS.CONNECTED */);
this.sendMessage({
topic: constants_1.TOPIC.CONNECTION,
action: constants_1.CONNECTION_ACTION.CHALLENGE,
url: this.originalUrl,
protocolVersion: '0.1a',
sdkVersion: pkg.version,
sdkType: 'javascript'
});
this.stateMachine.transition("challenge" /* TRANSITIONS.CHALLENGE */);
};
/**
* Callback for generic connection errors. Forwards
* the error to the client.
*
* The connection is considered broken once this method has been
* invoked.
*/
Connection.prototype.onError = function (error) {
var _this = this;
/*
* If the implementation isn't listening on the error event this will throw
* an error. So let's defer it to allow the reconnection to kick in.
*/
setTimeout(function () {
var msg;
if (error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED') {
msg = "Can't connect! Deepstream server unreachable on ".concat(_this.originalUrl);
}
else {
msg = error;
}
_this.services.logger.error({ topic: constants_1.TOPIC.CONNECTION }, constants_1.EVENT.CONNECTION_ERROR, msg);
}, 1);
this.services.timerRegistry.remove(this.heartbeatIntervalTimeout);
this.stateMachine.transition("error" /* TRANSITIONS.ERROR */);
this.tryReconnect();
};
/**
* Callback when the connection closes. This might have been a deliberate
* close triggered by the client or the result of the connection getting
* lost.
*
* In the latter case the client will try to reconnect using the configured
* strategy.
*/
Connection.prototype.onClose = function () {
this.services.timerRegistry.remove(this.heartbeatIntervalTimeout);
if (this.stateMachine.state === constants_1.CONNECTION_STATE.REDIRECTING) {
this.createEndpoint();
return;
}
if (this.stateMachine.state === constants_1.CONNECTION_STATE.CHALLENGE_DENIED ||
this.stateMachine.state === constants_1.CONNECTION_STATE.TOO_MANY_AUTH_ATTEMPTS ||
this.stateMachine.state === constants_1.CONNECTION_STATE.AUTHENTICATION_TIMEOUT) {
return;
}
if (this.stateMachine.state === constants_1.CONNECTION_STATE.CLOSING) {
this.stateMachine.transition("closed" /* TRANSITIONS.CLOSED */);
return;
}
if (this.stateMachine.state === constants_1.CONNECTION_STATE.PAUSING) {
this.stateMachine.transition("offline" /* TRANSITIONS.OFFLINE */);
return;
}
this.stateMachine.transition("connection-lost" /* TRANSITIONS.LOST */);
this.tryReconnect();
};
/**
* Callback for messages received on the connection.
*/
Connection.prototype.onMessages = function (parseResults) {
var _this = this;
parseResults.forEach(function (parseResult) {
if (parseResult.parseError) {
_this.services.logger.error({ topic: constants_1.TOPIC.PARSER }, parseResult.action, parseResult.raw && parseResult.raw.toString());
return;
}
var message = parseResult;
// NOTE: parseData mutates the message object adding the parsedData property and returns true when succesful
var res = (0, message_parser_1.parseData)(message);
if (res !== true) {
_this.services.logger.error({ topic: constants_1.TOPIC.PARSER }, constants_1.PARSER_ACTION.INVALID_MESSAGE, res);
}
if (message === null) {
return;
}
if (message.topic === constants_1.TOPIC.CONNECTION) {
_this.handleConnectionResponse(message);
return;
}
if (message.topic === constants_1.TOPIC.AUTH) {
_this.handleAuthResponse(message);
return;
}
var handler = _this.handlers.get(message.topic);
if (!handler) {
// this should never happen
return;
}
handler(message);
});
};
/**
* Sends authentication params to the server. Please note, this
* doesn't use the queued message mechanism, but rather sends the message directly
*/
Connection.prototype.sendAuthParams = function () {
this.stateMachine.transition("authenticate" /* TRANSITIONS.AUTHENTICATE */);
this.sendMessage({
topic: constants_1.TOPIC.AUTH,
action: constants_1.AUTH_ACTION.REQUEST,
parsedData: this.authParams
});
};
/**
* Ensures that a heartbeat was not missed more than once, otherwise it considers the connection
* to have been lost and closes it for reconnection.
*/
Connection.prototype.checkHeartBeat = function () {
var heartBeatTolerance = this.options.heartbeatInterval * 2;
if (!this.endpoint) {
return;
}
if (this.endpoint.getTimeSinceLastMessage() > heartBeatTolerance) {
this.services.timerRegistry.remove(this.heartbeatIntervalTimeout);
this.services.logger.error({ topic: constants_1.TOPIC.CONNECTION }, constants_1.EVENT.HEARTBEAT_TIMEOUT);
this.endpoint.close();
return;
}
this.heartbeatIntervalTimeout = this.services.timerRegistry.add({
duration: this.options.heartbeatInterval,
callback: this.checkHeartBeat,
context: this
});
};
/**
* If the connection drops or is closed in error this
* method schedules increasing reconnection intervals
*
* If the number of failed reconnection attempts exceeds
* options.maxReconnectAttempts the connection is closed
*/
Connection.prototype.tryReconnect = function () {
if (this.reconnectTimeout !== null) {
return;
}
if (this.reconnectionAttempt < this.options.maxReconnectAttempts) {
this.stateMachine.transition("reconnect" /* TRANSITIONS.RECONNECT */);
this.reconnectTimeout = this.services.timerRegistry.add({
callback: this.tryOpen,
context: this,
duration: Math.min(this.options.maxReconnectInterval, this.options.reconnectIntervalIncrement * this.reconnectionAttempt)
});
this.reconnectionAttempt++;
return;
}
this.emitter.emit(constants_1.EVENT[constants_1.EVENT.MAX_RECONNECTION_ATTEMPTS_REACHED], this.reconnectionAttempt);
this.clearReconnect();
this.close();
};
/**
* Attempts to open a errourosly closed connection
*/
Connection.prototype.tryOpen = function () {
if (this.stateMachine.state !== constants_1.CONNECTION_STATE.REDIRECTING) {
this.url = this.originalUrl;
}
this.createEndpoint();
this.reconnectTimeout = null;
};
/**
* Stops all further reconnection attempts,
* either because the connection is open again
* or because the maximal number of reconnection
* attempts has been exceeded
*/
Connection.prototype.clearReconnect = function () {
this.services.timerRegistry.remove(this.reconnectTimeout);
this.reconnectTimeout = null;
this.reconnectionAttempt = 0;
};
/**
* The connection response will indicate whether the deepstream connection
* can be used or if it should be forwarded to another instance. This
* allows us to introduce load-balancing if needed.
*
* If authentication parameters are already provided this will kick of
* authentication immediately. The actual 'open' event won't be emitted
* by the client until the authentication is successful.
*
* If a challenge is recieved, the user will send the url to the server
* in response to get the appropriate redirect. If the URL is invalid the
* server will respond with a REJECTION resulting in the client connection
* being permanently closed.
*
* If a redirect is recieved, this connection is closed and updated with
* a connection to the url supplied in the message.
*/
Connection.prototype.handleConnectionResponse = function (message) {
if (message.action === constants_1.CONNECTION_ACTION.ACCEPT) {
this.stateMachine.transition("accepted" /* TRANSITIONS.CHALLENGE_ACCEPTED */);
return;
}
if (message.action === constants_1.CONNECTION_ACTION.REJECT) {
this.stateMachine.transition("challenge-denied" /* TRANSITIONS.CHALLENGE_DENIED */);
if (this.endpoint) {
this.endpoint.close();
}
return;
}
if (message.action === constants_1.CONNECTION_ACTION.REDIRECT) {
this.url = message.url;
this.stateMachine.transition("redirected" /* TRANSITIONS.CONNECTION_REDIRECTED */);
if (this.endpoint) {
this.endpoint.close();
}
return;
}
if (message.action === constants_1.CONNECTION_ACTION.AUTHENTICATION_TIMEOUT) {
this.stateMachine.transition("authentication-timeout" /* TRANSITIONS.AUTHENTICATION_TIMEOUT */);
this.services.logger.error(message);
}
};
/**
* Callback for messages received for the AUTH topic. If
* the authentication was successful this method will
* open the connection and send all messages that the client
* tried to send so far.
*/
Connection.prototype.handleAuthResponse = function (message) {
if (message.action === constants_1.AUTH_ACTION.TOO_MANY_AUTH_ATTEMPTS) {
this.stateMachine.transition("too-many-auth-attempts" /* TRANSITIONS.TOO_MANY_AUTH_ATTEMPTS */);
this.services.logger.error(message);
return;
}
if (message.action === constants_1.AUTH_ACTION.AUTH_UNSUCCESSFUL) {
this.stateMachine.transition("unsuccesful-login" /* TRANSITIONS.UNSUCCESFUL_LOGIN */);
this.onAuthUnSuccessful(message.parsedData);
return;
}
if (message.action === constants_1.AUTH_ACTION.AUTH_SUCCESSFUL) {
this.stateMachine.transition("succesful-login" /* TRANSITIONS.SUCCESFUL_LOGIN */);
this.onAuthSuccessful(message.parsedData);
return;
}
};
Connection.prototype.onAwaitingAuthentication = function () {
if (this.authParams) {
this.sendAuthParams();
}
};
Connection.prototype.onAuthSuccessful = function (clientData) {
this.updateClientData(clientData);
if (this.resumeCallback) {
this.resumeCallback();
this.resumeCallback = null;
}
if (this.authCallback === null) {
return;
}
this.authCallback(true, this.clientData);
this.authCallback = null;
};
Connection.prototype.onAuthUnSuccessful = function (clientData) {
var reason = { reason: clientData || constants_1.EVENT[constants_1.EVENT.INVALID_AUTHENTICATION_DETAILS] };
if (this.resumeCallback) {
this.resumeCallback(reason);
this.resumeCallback = null;
}
if (this.authCallback === null) {
this.emitter.emit(constants_1.EVENT.REAUTHENTICATION_FAILURE, reason);
return;
}
this.authCallback(false, reason);
this.authCallback = null;
};
Connection.prototype.updateClientData = function (data) {
var newClientData = data || null;
if (this.clientData === null &&
(newClientData === null || Object.keys(newClientData).length === 0)) {
return;
}
if (!utils.deepEquals(this.clientData, data)) {
this.emitter.emit(constants_1.EVENT.CLIENT_DATA_CHANGED, Object.assign({}, newClientData));
this.clientData = newClientData;
}
};
return Connection;
}());
exports.Connection = Connection;