UNPKG

@deepstream/client

Version:
515 lines (514 loc) 24.9 kB
"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;