UNPKG

ran-boilerplate

Version:

React . Apollo (GraphQL) . Next.js Toolkit

494 lines (492 loc) 21.3 kB
"use strict"; /** * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); var util_1 = require("../core/util/util"); var storage_1 = require("../core/storage/storage"); var Constants_1 = require("./Constants"); var TransportManager_1 = require("./TransportManager"); // Abort upgrade attempt if it takes longer than 60s. var UPGRADE_TIMEOUT = 60000; // For some transports (WebSockets), we need to "validate" the transport by exchanging a few requests and responses. // If we haven't sent enough requests within 5s, we'll start sending noop ping requests. var DELAY_BEFORE_SENDING_EXTRA_REQUESTS = 5000; // If the initial data sent triggers a lot of bandwidth (i.e. it's a large put or a listen for a large amount of data) // then we may not be able to exchange our ping/pong requests within the healthy timeout. So if we reach the timeout // but we've sent/received enough bytes, we don't cancel the connection. var BYTES_SENT_HEALTHY_OVERRIDE = 10 * 1024; var BYTES_RECEIVED_HEALTHY_OVERRIDE = 100 * 1024; var MESSAGE_TYPE = 't'; var MESSAGE_DATA = 'd'; var CONTROL_SHUTDOWN = 's'; var CONTROL_RESET = 'r'; var CONTROL_ERROR = 'e'; var CONTROL_PONG = 'o'; var SWITCH_ACK = 'a'; var END_TRANSMISSION = 'n'; var PING = 'p'; var SERVER_HELLO = 'h'; /** * Creates a new real-time connection to the server using whichever method works * best in the current browser. * * @constructor */ var Connection = /** @class */ (function () { /** * @param {!string} id - an id for this connection * @param {!RepoInfo} repoInfo_ - the info for the endpoint to connect to * @param {function(Object)} onMessage_ - the callback to be triggered when a server-push message arrives * @param {function(number, string)} onReady_ - the callback to be triggered when this connection is ready to send messages. * @param {function()} onDisconnect_ - the callback to be triggered when a connection was lost * @param {function(string)} onKill_ - the callback to be triggered when this connection has permanently shut down. * @param {string=} lastSessionId - last session id in persistent connection. is used to clean up old session in real-time server */ function Connection(id, repoInfo_, onMessage_, onReady_, onDisconnect_, onKill_, lastSessionId) { this.id = id; this.repoInfo_ = repoInfo_; this.onMessage_ = onMessage_; this.onReady_ = onReady_; this.onDisconnect_ = onDisconnect_; this.onKill_ = onKill_; this.lastSessionId = lastSessionId; this.connectionCount = 0; this.pendingDataMessages = []; this.state_ = 0 /* CONNECTING */; this.log_ = util_1.logWrapper('c:' + this.id + ':'); this.transportManager_ = new TransportManager_1.TransportManager(repoInfo_); this.log_('Connection created'); this.start_(); } /** * Starts a connection attempt * @private */ Connection.prototype.start_ = function () { var _this = this; var conn = this.transportManager_.initialTransport(); this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, undefined, this.lastSessionId); // For certain transports (WebSockets), we need to send and receive several messages back and forth before we // can consider the transport healthy. this.primaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0; var onMessageReceived = this.connReceiver_(this.conn_); var onConnectionLost = this.disconnReceiver_(this.conn_); this.tx_ = this.conn_; this.rx_ = this.conn_; this.secondaryConn_ = null; this.isHealthy_ = false; /* * Firefox doesn't like when code from one iframe tries to create another iframe by way of the parent frame. * This can occur in the case of a redirect, i.e. we guessed wrong on what server to connect to and received a reset. * Somehow, setTimeout seems to make this ok. That doesn't make sense from a security perspective, since you should * still have the context of your originating frame. */ setTimeout(function () { // this.conn_ gets set to null in some of the tests. Check to make sure it still exists before using it _this.conn_ && _this.conn_.open(onMessageReceived, onConnectionLost); }, Math.floor(0)); var healthyTimeout_ms = conn['healthyTimeout'] || 0; if (healthyTimeout_ms > 0) { this.healthyTimeout_ = util_1.setTimeoutNonBlocking(function () { _this.healthyTimeout_ = null; if (!_this.isHealthy_) { if (_this.conn_ && _this.conn_.bytesReceived > BYTES_RECEIVED_HEALTHY_OVERRIDE) { _this.log_('Connection exceeded healthy timeout but has received ' + _this.conn_.bytesReceived + ' bytes. Marking connection healthy.'); _this.isHealthy_ = true; _this.conn_.markConnectionHealthy(); } else if (_this.conn_ && _this.conn_.bytesSent > BYTES_SENT_HEALTHY_OVERRIDE) { _this.log_('Connection exceeded healthy timeout but has sent ' + _this.conn_.bytesSent + ' bytes. Leaving connection alive.'); // NOTE: We don't want to mark it healthy, since we have no guarantee that the bytes have made it to // the server. } else { _this.log_('Closing unhealthy connection after timeout.'); _this.close(); } } }, Math.floor(healthyTimeout_ms)); } }; /** * @return {!string} * @private */ Connection.prototype.nextTransportId_ = function () { return 'c:' + this.id + ':' + this.connectionCount++; }; Connection.prototype.disconnReceiver_ = function (conn) { var _this = this; return function (everConnected) { if (conn === _this.conn_) { _this.onConnectionLost_(everConnected); } else if (conn === _this.secondaryConn_) { _this.log_('Secondary connection lost.'); _this.onSecondaryConnectionLost_(); } else { _this.log_('closing an old connection'); } }; }; Connection.prototype.connReceiver_ = function (conn) { var _this = this; return function (message) { if (_this.state_ != 2 /* DISCONNECTED */) { if (conn === _this.rx_) { _this.onPrimaryMessageReceived_(message); } else if (conn === _this.secondaryConn_) { _this.onSecondaryMessageReceived_(message); } else { _this.log_('message on old connection'); } } }; }; /** * * @param {Object} dataMsg An arbitrary data message to be sent to the server */ Connection.prototype.sendRequest = function (dataMsg) { // wrap in a data message envelope and send it on var msg = { t: 'd', d: dataMsg }; this.sendData_(msg); }; Connection.prototype.tryCleanupConnection = function () { if (this.tx_ === this.secondaryConn_ && this.rx_ === this.secondaryConn_) { this.log_('cleaning up and promoting a connection: ' + this.secondaryConn_.connId); this.conn_ = this.secondaryConn_; this.secondaryConn_ = null; // the server will shutdown the old connection } }; Connection.prototype.onSecondaryControl_ = function (controlData) { if (MESSAGE_TYPE in controlData) { var cmd = controlData[MESSAGE_TYPE]; if (cmd === SWITCH_ACK) { this.upgradeIfSecondaryHealthy_(); } else if (cmd === CONTROL_RESET) { // Most likely the session wasn't valid. Abandon the switch attempt this.log_('Got a reset on secondary, closing it'); this.secondaryConn_.close(); // If we were already using this connection for something, than we need to fully close if (this.tx_ === this.secondaryConn_ || this.rx_ === this.secondaryConn_) { this.close(); } } else if (cmd === CONTROL_PONG) { this.log_('got pong on secondary.'); this.secondaryResponsesRequired_--; this.upgradeIfSecondaryHealthy_(); } } }; Connection.prototype.onSecondaryMessageReceived_ = function (parsedData) { var layer = util_1.requireKey('t', parsedData); var data = util_1.requireKey('d', parsedData); if (layer == 'c') { this.onSecondaryControl_(data); } else if (layer == 'd') { // got a data message, but we're still second connection. Need to buffer it up this.pendingDataMessages.push(data); } else { throw new Error('Unknown protocol layer: ' + layer); } }; Connection.prototype.upgradeIfSecondaryHealthy_ = function () { if (this.secondaryResponsesRequired_ <= 0) { this.log_('Secondary connection is healthy.'); this.isHealthy_ = true; this.secondaryConn_.markConnectionHealthy(); this.proceedWithUpgrade_(); } else { // Send a ping to make sure the connection is healthy. this.log_('sending ping on secondary.'); this.secondaryConn_.send({ t: 'c', d: { t: PING, d: {} } }); } }; Connection.prototype.proceedWithUpgrade_ = function () { // tell this connection to consider itself open this.secondaryConn_.start(); // send ack this.log_('sending client ack on secondary'); this.secondaryConn_.send({ t: 'c', d: { t: SWITCH_ACK, d: {} } }); // send end packet on primary transport, switch to sending on this one // can receive on this one, buffer responses until end received on primary transport this.log_('Ending transmission on primary'); this.conn_.send({ t: 'c', d: { t: END_TRANSMISSION, d: {} } }); this.tx_ = this.secondaryConn_; this.tryCleanupConnection(); }; Connection.prototype.onPrimaryMessageReceived_ = function (parsedData) { // Must refer to parsedData properties in quotes, so closure doesn't touch them. var layer = util_1.requireKey('t', parsedData); var data = util_1.requireKey('d', parsedData); if (layer == 'c') { this.onControl_(data); } else if (layer == 'd') { this.onDataMessage_(data); } }; Connection.prototype.onDataMessage_ = function (message) { this.onPrimaryResponse_(); // We don't do anything with data messages, just kick them up a level this.onMessage_(message); }; Connection.prototype.onPrimaryResponse_ = function () { if (!this.isHealthy_) { this.primaryResponsesRequired_--; if (this.primaryResponsesRequired_ <= 0) { this.log_('Primary connection is healthy.'); this.isHealthy_ = true; this.conn_.markConnectionHealthy(); } } }; Connection.prototype.onControl_ = function (controlData) { var cmd = util_1.requireKey(MESSAGE_TYPE, controlData); if (MESSAGE_DATA in controlData) { var payload = controlData[MESSAGE_DATA]; if (cmd === SERVER_HELLO) { this.onHandshake_(payload); } else if (cmd === END_TRANSMISSION) { this.log_('recvd end transmission on primary'); this.rx_ = this.secondaryConn_; for (var i = 0; i < this.pendingDataMessages.length; ++i) { this.onDataMessage_(this.pendingDataMessages[i]); } this.pendingDataMessages = []; this.tryCleanupConnection(); } else if (cmd === CONTROL_SHUTDOWN) { // This was previously the 'onKill' callback passed to the lower-level connection // payload in this case is the reason for the shutdown. Generally a human-readable error this.onConnectionShutdown_(payload); } else if (cmd === CONTROL_RESET) { // payload in this case is the host we should contact this.onReset_(payload); } else if (cmd === CONTROL_ERROR) { util_1.error('Server Error: ' + payload); } else if (cmd === CONTROL_PONG) { this.log_('got pong on primary.'); this.onPrimaryResponse_(); this.sendPingOnPrimaryIfNecessary_(); } else { util_1.error('Unknown control packet command: ' + cmd); } } }; /** * * @param {Object} handshake The handshake data returned from the server * @private */ Connection.prototype.onHandshake_ = function (handshake) { var timestamp = handshake.ts; var version = handshake.v; var host = handshake.h; this.sessionId = handshake.s; this.repoInfo_.updateHost(host); // if we've already closed the connection, then don't bother trying to progress further if (this.state_ == 0 /* CONNECTING */) { this.conn_.start(); this.onConnectionEstablished_(this.conn_, timestamp); if (Constants_1.PROTOCOL_VERSION !== version) { util_1.warn('Protocol version mismatch detected'); } // TODO: do we want to upgrade? when? maybe a delay? this.tryStartUpgrade_(); } }; Connection.prototype.tryStartUpgrade_ = function () { var conn = this.transportManager_.upgradeTransport(); if (conn) { this.startUpgrade_(conn); } }; Connection.prototype.startUpgrade_ = function (conn) { var _this = this; this.secondaryConn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.sessionId); // For certain transports (WebSockets), we need to send and receive several messages back and forth before we // can consider the transport healthy. this.secondaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0; var onMessage = this.connReceiver_(this.secondaryConn_); var onDisconnect = this.disconnReceiver_(this.secondaryConn_); this.secondaryConn_.open(onMessage, onDisconnect); // If we haven't successfully upgraded after UPGRADE_TIMEOUT, give up and kill the secondary. util_1.setTimeoutNonBlocking(function () { if (_this.secondaryConn_) { _this.log_('Timed out trying to upgrade.'); _this.secondaryConn_.close(); } }, Math.floor(UPGRADE_TIMEOUT)); }; Connection.prototype.onReset_ = function (host) { this.log_('Reset packet received. New host: ' + host); this.repoInfo_.updateHost(host); // TODO: if we're already "connected", we need to trigger a disconnect at the next layer up. // We don't currently support resets after the connection has already been established if (this.state_ === 1 /* CONNECTED */) { this.close(); } else { // Close whatever connections we have open and start again. this.closeConnections_(); this.start_(); } }; Connection.prototype.onConnectionEstablished_ = function (conn, timestamp) { var _this = this; this.log_('Realtime connection established.'); this.conn_ = conn; this.state_ = 1 /* CONNECTED */; if (this.onReady_) { this.onReady_(timestamp, this.sessionId); this.onReady_ = null; } // If after 5 seconds we haven't sent enough requests to the server to get the connection healthy, // send some pings. if (this.primaryResponsesRequired_ === 0) { this.log_('Primary connection is healthy.'); this.isHealthy_ = true; } else { util_1.setTimeoutNonBlocking(function () { _this.sendPingOnPrimaryIfNecessary_(); }, Math.floor(DELAY_BEFORE_SENDING_EXTRA_REQUESTS)); } }; Connection.prototype.sendPingOnPrimaryIfNecessary_ = function () { // If the connection isn't considered healthy yet, we'll send a noop ping packet request. if (!this.isHealthy_ && this.state_ === 1 /* CONNECTED */) { this.log_('sending ping on primary.'); this.sendData_({ t: 'c', d: { t: PING, d: {} } }); } }; Connection.prototype.onSecondaryConnectionLost_ = function () { var conn = this.secondaryConn_; this.secondaryConn_ = null; if (this.tx_ === conn || this.rx_ === conn) { // we are relying on this connection already in some capacity. Therefore, a failure is real this.close(); } }; /** * * @param {boolean} everConnected Whether or not the connection ever reached a server. Used to determine if * we should flush the host cache * @private */ Connection.prototype.onConnectionLost_ = function (everConnected) { this.conn_ = null; // NOTE: IF you're seeing a Firefox error for this line, I think it might be because it's getting // called on window close and RealtimeState.CONNECTING is no longer defined. Just a guess. if (!everConnected && this.state_ === 0 /* CONNECTING */) { this.log_('Realtime connection failed.'); // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away if (this.repoInfo_.isCacheableHost()) { storage_1.PersistentStorage.remove('host:' + this.repoInfo_.host); // reset the internal host to what we would show the user, i.e. <ns>.firebaseio.com this.repoInfo_.internalHost = this.repoInfo_.host; } } else if (this.state_ === 1 /* CONNECTED */) { this.log_('Realtime connection lost.'); } this.close(); }; /** * * @param {string} reason * @private */ Connection.prototype.onConnectionShutdown_ = function (reason) { this.log_('Connection shutdown command received. Shutting down...'); if (this.onKill_) { this.onKill_(reason); this.onKill_ = null; } // We intentionally don't want to fire onDisconnect (kill is a different case), // so clear the callback. this.onDisconnect_ = null; this.close(); }; Connection.prototype.sendData_ = function (data) { if (this.state_ !== 1 /* CONNECTED */) { throw 'Connection is not connected'; } else { this.tx_.send(data); } }; /** * Cleans up this connection, calling the appropriate callbacks */ Connection.prototype.close = function () { if (this.state_ !== 2 /* DISCONNECTED */) { this.log_('Closing realtime connection.'); this.state_ = 2 /* DISCONNECTED */; this.closeConnections_(); if (this.onDisconnect_) { this.onDisconnect_(); this.onDisconnect_ = null; } } }; /** * * @private */ Connection.prototype.closeConnections_ = function () { this.log_('Shutting down all connections'); if (this.conn_) { this.conn_.close(); this.conn_ = null; } if (this.secondaryConn_) { this.secondaryConn_.close(); this.secondaryConn_ = null; } if (this.healthyTimeout_) { clearTimeout(this.healthyTimeout_); this.healthyTimeout_ = null; } }; return Connection; }()); exports.Connection = Connection; //# sourceMappingURL=Connection.js.map