ran-boilerplate
Version:
React . Apollo (GraphQL) . Next.js Toolkit
352 lines (350 loc) • 14.1 kB
JavaScript
"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 app_1 = require("@firebase/app");
var util_1 = require("@firebase/util");
var util_2 = require("../core/util/util");
var StatsManager_1 = require("../core/stats/StatsManager");
var Constants_1 = require("./Constants");
var util_3 = require("@firebase/util");
var storage_1 = require("../core/storage/storage");
var util_4 = require("@firebase/util");
var util_5 = require("@firebase/util");
var WEBSOCKET_MAX_FRAME_SIZE = 16384;
var WEBSOCKET_KEEPALIVE_INTERVAL = 45000;
var WebSocketImpl = null;
if (typeof MozWebSocket !== 'undefined') {
WebSocketImpl = MozWebSocket;
}
else if (typeof WebSocket !== 'undefined') {
WebSocketImpl = WebSocket;
}
function setWebSocketImpl(impl) {
WebSocketImpl = impl;
}
exports.setWebSocketImpl = setWebSocketImpl;
/**
* Create a new websocket connection with the given callbacks.
* @constructor
* @implements {Transport}
*/
var WebSocketConnection = /** @class */ (function () {
/**
* @param {string} connId identifier for this transport
* @param {RepoInfo} repoInfo The info for the websocket endpoint.
* @param {string=} transportSessionId Optional transportSessionId if this is connecting to an existing transport
* session
* @param {string=} lastSessionId Optional lastSessionId if there was a previous connection
*/
function WebSocketConnection(connId, repoInfo, transportSessionId, lastSessionId) {
this.connId = connId;
this.keepaliveTimer = null;
this.frames = null;
this.totalFrames = 0;
this.bytesSent = 0;
this.bytesReceived = 0;
this.log_ = util_2.logWrapper(this.connId);
this.stats_ = StatsManager_1.StatsManager.getCollection(repoInfo);
this.connURL = WebSocketConnection.connectionURL_(repoInfo, transportSessionId, lastSessionId);
}
/**
* @param {RepoInfo} repoInfo The info for the websocket endpoint.
* @param {string=} transportSessionId Optional transportSessionId if this is connecting to an existing transport
* session
* @param {string=} lastSessionId Optional lastSessionId if there was a previous connection
* @return {string} connection url
* @private
*/
WebSocketConnection.connectionURL_ = function (repoInfo, transportSessionId, lastSessionId) {
var urlParams = {};
urlParams[Constants_1.VERSION_PARAM] = Constants_1.PROTOCOL_VERSION;
if (!util_5.isNodeSdk() &&
typeof location !== 'undefined' &&
location.href &&
location.href.indexOf(Constants_1.FORGE_DOMAIN) !== -1) {
urlParams[Constants_1.REFERER_PARAM] = Constants_1.FORGE_REF;
}
if (transportSessionId) {
urlParams[Constants_1.TRANSPORT_SESSION_PARAM] = transportSessionId;
}
if (lastSessionId) {
urlParams[Constants_1.LAST_SESSION_PARAM] = lastSessionId;
}
return repoInfo.connectionURL(Constants_1.WEBSOCKET, urlParams);
};
/**
*
* @param onMessage Callback when messages arrive
* @param onDisconnect Callback with connection lost.
*/
WebSocketConnection.prototype.open = function (onMessage, onDisconnect) {
var _this = this;
this.onDisconnect = onDisconnect;
this.onMessage = onMessage;
this.log_('Websocket connecting to ' + this.connURL);
this.everConnected_ = false;
// Assume failure until proven otherwise.
storage_1.PersistentStorage.set('previous_websocket_failure', true);
try {
if (util_5.isNodeSdk()) {
var device = util_3.CONSTANTS.NODE_ADMIN ? 'AdminNode' : 'Node';
// UA Format: Firebase/<wire_protocol>/<sdk_version>/<platform>/<device>
var options = {
headers: {
'User-Agent': "Firebase/" + Constants_1.PROTOCOL_VERSION + "/" + app_1.default.SDK_VERSION + "/" + process.platform + "/" + device
}
};
// Plumb appropriate http_proxy environment variable into faye-websocket if it exists.
var env = process['env'];
var proxy = this.connURL.indexOf('wss://') == 0
? env['HTTPS_PROXY'] || env['https_proxy']
: env['HTTP_PROXY'] || env['http_proxy'];
if (proxy) {
options['proxy'] = { origin: proxy };
}
this.mySock = new WebSocketImpl(this.connURL, [], options);
}
else {
this.mySock = new WebSocketImpl(this.connURL);
}
}
catch (e) {
this.log_('Error instantiating WebSocket.');
var error = e.message || e.data;
if (error) {
this.log_(error);
}
this.onClosed_();
return;
}
this.mySock.onopen = function () {
_this.log_('Websocket connected.');
_this.everConnected_ = true;
};
this.mySock.onclose = function () {
_this.log_('Websocket connection was disconnected.');
_this.mySock = null;
_this.onClosed_();
};
this.mySock.onmessage = function (m) {
_this.handleIncomingFrame(m);
};
this.mySock.onerror = function (e) {
_this.log_('WebSocket error. Closing connection.');
var error = e.message || e.data;
if (error) {
_this.log_(error);
}
_this.onClosed_();
};
};
/**
* No-op for websockets, we don't need to do anything once the connection is confirmed as open
*/
WebSocketConnection.prototype.start = function () { };
WebSocketConnection.forceDisallow = function () {
WebSocketConnection.forceDisallow_ = true;
};
WebSocketConnection.isAvailable = function () {
var isOldAndroid = false;
if (typeof navigator !== 'undefined' && navigator.userAgent) {
var oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/;
var oldAndroidMatch = navigator.userAgent.match(oldAndroidRegex);
if (oldAndroidMatch && oldAndroidMatch.length > 1) {
if (parseFloat(oldAndroidMatch[1]) < 4.4) {
isOldAndroid = true;
}
}
}
return (!isOldAndroid &&
WebSocketImpl !== null &&
!WebSocketConnection.forceDisallow_);
};
/**
* Returns true if we previously failed to connect with this transport.
* @return {boolean}
*/
WebSocketConnection.previouslyFailed = function () {
// If our persistent storage is actually only in-memory storage,
// we default to assuming that it previously failed to be safe.
return (storage_1.PersistentStorage.isInMemoryStorage ||
storage_1.PersistentStorage.get('previous_websocket_failure') === true);
};
WebSocketConnection.prototype.markConnectionHealthy = function () {
storage_1.PersistentStorage.remove('previous_websocket_failure');
};
WebSocketConnection.prototype.appendFrame_ = function (data) {
this.frames.push(data);
if (this.frames.length == this.totalFrames) {
var fullMess = this.frames.join('');
this.frames = null;
var jsonMess = util_4.jsonEval(fullMess);
//handle the message
this.onMessage(jsonMess);
}
};
/**
* @param {number} frameCount The number of frames we are expecting from the server
* @private
*/
WebSocketConnection.prototype.handleNewFrameCount_ = function (frameCount) {
this.totalFrames = frameCount;
this.frames = [];
};
/**
* Attempts to parse a frame count out of some text. If it can't, assumes a value of 1
* @param {!String} data
* @return {?String} Any remaining data to be process, or null if there is none
* @private
*/
WebSocketConnection.prototype.extractFrameCount_ = function (data) {
util_1.assert(this.frames === null, 'We already have a frame buffer');
// TODO: The server is only supposed to send up to 9999 frames (i.e. length <= 4), but that isn't being enforced
// currently. So allowing larger frame counts (length <= 6). See https://app.asana.com/0/search/8688598998380/8237608042508
if (data.length <= 6) {
var frameCount = Number(data);
if (!isNaN(frameCount)) {
this.handleNewFrameCount_(frameCount);
return null;
}
}
this.handleNewFrameCount_(1);
return data;
};
/**
* Process a websocket frame that has arrived from the server.
* @param mess The frame data
*/
WebSocketConnection.prototype.handleIncomingFrame = function (mess) {
if (this.mySock === null)
return; // Chrome apparently delivers incoming packets even after we .close() the connection sometimes.
var data = mess['data'];
this.bytesReceived += data.length;
this.stats_.incrementCounter('bytes_received', data.length);
this.resetKeepAlive();
if (this.frames !== null) {
// we're buffering
this.appendFrame_(data);
}
else {
// try to parse out a frame count, otherwise, assume 1 and process it
var remainingData = this.extractFrameCount_(data);
if (remainingData !== null) {
this.appendFrame_(remainingData);
}
}
};
/**
* Send a message to the server
* @param {Object} data The JSON object to transmit
*/
WebSocketConnection.prototype.send = function (data) {
this.resetKeepAlive();
var dataStr = util_4.stringify(data);
this.bytesSent += dataStr.length;
this.stats_.incrementCounter('bytes_sent', dataStr.length);
//We can only fit a certain amount in each websocket frame, so we need to split this request
//up into multiple pieces if it doesn't fit in one request.
var dataSegs = util_2.splitStringBySize(dataStr, WEBSOCKET_MAX_FRAME_SIZE);
//Send the length header
if (dataSegs.length > 1) {
this.sendString_(String(dataSegs.length));
}
//Send the actual data in segments.
for (var i = 0; i < dataSegs.length; i++) {
this.sendString_(dataSegs[i]);
}
};
WebSocketConnection.prototype.shutdown_ = function () {
this.isClosed_ = true;
if (this.keepaliveTimer) {
clearInterval(this.keepaliveTimer);
this.keepaliveTimer = null;
}
if (this.mySock) {
this.mySock.close();
this.mySock = null;
}
};
WebSocketConnection.prototype.onClosed_ = function () {
if (!this.isClosed_) {
this.log_('WebSocket is closing itself');
this.shutdown_();
// since this is an internal close, trigger the close listener
if (this.onDisconnect) {
this.onDisconnect(this.everConnected_);
this.onDisconnect = null;
}
}
};
/**
* External-facing close handler.
* Close the websocket and kill the connection.
*/
WebSocketConnection.prototype.close = function () {
if (!this.isClosed_) {
this.log_('WebSocket is being closed');
this.shutdown_();
}
};
/**
* Kill the current keepalive timer and start a new one, to ensure that it always fires N seconds after
* the last activity.
*/
WebSocketConnection.prototype.resetKeepAlive = function () {
var _this = this;
clearInterval(this.keepaliveTimer);
this.keepaliveTimer = setInterval(function () {
//If there has been no websocket activity for a while, send a no-op
if (_this.mySock) {
_this.sendString_('0');
}
_this.resetKeepAlive();
}, Math.floor(WEBSOCKET_KEEPALIVE_INTERVAL));
};
/**
* Send a string over the websocket.
*
* @param {string} str String to send.
* @private
*/
WebSocketConnection.prototype.sendString_ = function (str) {
// Firefox seems to sometimes throw exceptions (NS_ERROR_UNEXPECTED) from websocket .send()
// calls for some unknown reason. We treat these as an error and disconnect.
// See https://app.asana.com/0/58926111402292/68021340250410
try {
this.mySock.send(str);
}
catch (e) {
this.log_('Exception thrown from WebSocket.send():', e.message || e.data, 'Closing connection.');
setTimeout(this.onClosed_.bind(this), 0);
}
};
/**
* Number of response before we consider the connection "healthy."
* @type {number}
*/
WebSocketConnection.responsesRequiredToBeHealthy = 2;
/**
* Time to wait for the connection te become healthy before giving up.
* @type {number}
*/
WebSocketConnection.healthyTimeout = 30000;
return WebSocketConnection;
}());
exports.WebSocketConnection = WebSocketConnection;
//# sourceMappingURL=WebSocketConnection.js.map