ran-boilerplate
Version:
React . Apollo (GraphQL) . Next.js Toolkit
630 lines (628 loc) • 29.3 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 util_1 = require("../core/util/util");
var CountedSet_1 = require("../core/util/CountedSet");
var StatsManager_1 = require("../core/stats/StatsManager");
var PacketReceiver_1 = require("./polling/PacketReceiver");
var Constants_1 = require("./Constants");
var util_2 = require("@firebase/util");
var util_3 = require("@firebase/util");
// URL query parameters associated with longpolling
exports.FIREBASE_LONGPOLL_START_PARAM = 'start';
exports.FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close';
exports.FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand';
exports.FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB';
exports.FIREBASE_LONGPOLL_ID_PARAM = 'id';
exports.FIREBASE_LONGPOLL_PW_PARAM = 'pw';
exports.FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser';
exports.FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb';
exports.FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg';
exports.FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts';
exports.FIREBASE_LONGPOLL_DATA_PARAM = 'd';
exports.FIREBASE_LONGPOLL_DISCONN_FRAME_PARAM = 'disconn';
exports.FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM = 'dframe';
//Data size constants.
//TODO: Perf: the maximum length actually differs from browser to browser.
// We should check what browser we're on and set accordingly.
var MAX_URL_DATA_SIZE = 1870;
var SEG_HEADER_SIZE = 30; //ie: &seg=8299234&ts=982389123&d=
var MAX_PAYLOAD_SIZE = MAX_URL_DATA_SIZE - SEG_HEADER_SIZE;
/**
* Keepalive period
* send a fresh request at minimum every 25 seconds. Opera has a maximum request
* length of 30 seconds that we can't exceed.
* @const
* @type {number}
*/
var KEEPALIVE_REQUEST_INTERVAL = 25000;
/**
* How long to wait before aborting a long-polling connection attempt.
* @const
* @type {number}
*/
var LP_CONNECT_TIMEOUT = 30000;
/**
* This class manages a single long-polling connection.
*
* @constructor
* @implements {Transport}
*/
var BrowserPollConnection = /** @class */ (function () {
/**
* @param {string} connId An identifier for this connection, used for logging
* @param {RepoInfo} repoInfo The info for the endpoint to send data to.
* @param {string=} transportSessionId Optional transportSessionid if we are reconnecting for an existing
* transport session
* @param {string=} lastSessionId Optional lastSessionId if the PersistentConnection has already created a
* connection previously
*/
function BrowserPollConnection(connId, repoInfo, transportSessionId, lastSessionId) {
this.connId = connId;
this.repoInfo = repoInfo;
this.transportSessionId = transportSessionId;
this.lastSessionId = lastSessionId;
this.bytesSent = 0;
this.bytesReceived = 0;
this.everConnected_ = false;
this.log_ = util_1.logWrapper(connId);
this.stats_ = StatsManager_1.StatsManager.getCollection(repoInfo);
this.urlFn = function (params) {
return repoInfo.connectionURL(Constants_1.LONG_POLLING, params);
};
}
/**
*
* @param {function(Object)} onMessage Callback when messages arrive
* @param {function()} onDisconnect Callback with connection lost.
*/
BrowserPollConnection.prototype.open = function (onMessage, onDisconnect) {
var _this = this;
this.curSegmentNum = 0;
this.onDisconnect_ = onDisconnect;
this.myPacketOrderer = new PacketReceiver_1.PacketReceiver(onMessage);
this.isClosed_ = false;
this.connectTimeoutTimer_ = setTimeout(function () {
_this.log_('Timed out trying to connect.');
// Make sure we clear the host cache
_this.onClosed_();
_this.connectTimeoutTimer_ = null;
}, Math.floor(LP_CONNECT_TIMEOUT));
// Ensure we delay the creation of the iframe until the DOM is loaded.
util_1.executeWhenDOMReady(function () {
if (_this.isClosed_)
return;
//Set up a callback that gets triggered once a connection is set up.
_this.scriptTagHolder = new FirebaseIFrameScriptHolder(function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var command = args[0], arg1 = args[1], arg2 = args[2], arg3 = args[3], arg4 = args[4];
_this.incrementIncomingBytes_(args);
if (!_this.scriptTagHolder)
return; // we closed the connection.
if (_this.connectTimeoutTimer_) {
clearTimeout(_this.connectTimeoutTimer_);
_this.connectTimeoutTimer_ = null;
}
_this.everConnected_ = true;
if (command == exports.FIREBASE_LONGPOLL_START_PARAM) {
_this.id = arg1;
_this.password = arg2;
}
else if (command === exports.FIREBASE_LONGPOLL_CLOSE_COMMAND) {
// Don't clear the host cache. We got a response from the server, so we know it's reachable
if (arg1) {
// We aren't expecting any more data (other than what the server's already in the process of sending us
// through our already open polls), so don't send any more.
_this.scriptTagHolder.sendNewPolls = false;
// arg1 in this case is the last response number sent by the server. We should try to receive
// all of the responses up to this one before closing
_this.myPacketOrderer.closeAfter(arg1, function () {
_this.onClosed_();
});
}
else {
_this.onClosed_();
}
}
else {
throw new Error('Unrecognized command received: ' + command);
}
}, function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var pN = args[0], data = args[1];
_this.incrementIncomingBytes_(args);
_this.myPacketOrderer.handleResponse(pN, data);
}, function () {
_this.onClosed_();
}, _this.urlFn);
//Send the initial request to connect. The serial number is simply to keep the browser from pulling previous results
//from cache.
var urlParams = {};
urlParams[exports.FIREBASE_LONGPOLL_START_PARAM] = 't';
urlParams[exports.FIREBASE_LONGPOLL_SERIAL_PARAM] = Math.floor(Math.random() * 100000000);
if (_this.scriptTagHolder.uniqueCallbackIdentifier)
urlParams[exports.FIREBASE_LONGPOLL_CALLBACK_ID_PARAM] = _this.scriptTagHolder.uniqueCallbackIdentifier;
urlParams[Constants_1.VERSION_PARAM] = Constants_1.PROTOCOL_VERSION;
if (_this.transportSessionId) {
urlParams[Constants_1.TRANSPORT_SESSION_PARAM] = _this.transportSessionId;
}
if (_this.lastSessionId) {
urlParams[Constants_1.LAST_SESSION_PARAM] = _this.lastSessionId;
}
if (!util_3.isNodeSdk() &&
typeof location !== 'undefined' &&
location.href &&
location.href.indexOf(Constants_1.FORGE_DOMAIN) !== -1) {
urlParams[Constants_1.REFERER_PARAM] = Constants_1.FORGE_REF;
}
var connectURL = _this.urlFn(urlParams);
_this.log_('Connecting via long-poll to ' + connectURL);
_this.scriptTagHolder.addTag(connectURL, function () {
/* do nothing */
});
});
};
/**
* Call this when a handshake has completed successfully and we want to consider the connection established
*/
BrowserPollConnection.prototype.start = function () {
this.scriptTagHolder.startLongPoll(this.id, this.password);
this.addDisconnectPingFrame(this.id, this.password);
};
/**
* Forces long polling to be considered as a potential transport
*/
BrowserPollConnection.forceAllow = function () {
BrowserPollConnection.forceAllow_ = true;
};
/**
* Forces longpolling to not be considered as a potential transport
*/
BrowserPollConnection.forceDisallow = function () {
BrowserPollConnection.forceDisallow_ = true;
};
// Static method, use string literal so it can be accessed in a generic way
BrowserPollConnection.isAvailable = function () {
// NOTE: In React-Native there's normally no 'document', but if you debug a React-Native app in
// the Chrome debugger, 'document' is defined, but document.createElement is null (2015/06/08).
return (BrowserPollConnection.forceAllow_ ||
(!BrowserPollConnection.forceDisallow_ &&
typeof document !== 'undefined' &&
document.createElement != null &&
!util_1.isChromeExtensionContentScript() &&
!util_1.isWindowsStoreApp() &&
!util_3.isNodeSdk()));
};
/**
* No-op for polling
*/
BrowserPollConnection.prototype.markConnectionHealthy = function () { };
/**
* Stops polling and cleans up the iframe
* @private
*/
BrowserPollConnection.prototype.shutdown_ = function () {
this.isClosed_ = true;
if (this.scriptTagHolder) {
this.scriptTagHolder.close();
this.scriptTagHolder = null;
}
//remove the disconnect frame, which will trigger an XHR call to the server to tell it we're leaving.
if (this.myDisconnFrame) {
document.body.removeChild(this.myDisconnFrame);
this.myDisconnFrame = null;
}
if (this.connectTimeoutTimer_) {
clearTimeout(this.connectTimeoutTimer_);
this.connectTimeoutTimer_ = null;
}
};
/**
* Triggered when this transport is closed
* @private
*/
BrowserPollConnection.prototype.onClosed_ = function () {
if (!this.isClosed_) {
this.log_('Longpoll is closing itself');
this.shutdown_();
if (this.onDisconnect_) {
this.onDisconnect_(this.everConnected_);
this.onDisconnect_ = null;
}
}
};
/**
* External-facing close handler. RealTime has requested we shut down. Kill our connection and tell the server
* that we've left.
*/
BrowserPollConnection.prototype.close = function () {
if (!this.isClosed_) {
this.log_('Longpoll is being closed.');
this.shutdown_();
}
};
/**
* Send the JSON object down to the server. It will need to be stringified, base64 encoded, and then
* broken into chunks (since URLs have a small maximum length).
* @param {!Object} data The JSON data to transmit.
*/
BrowserPollConnection.prototype.send = function (data) {
var dataStr = util_2.stringify(data);
this.bytesSent += dataStr.length;
this.stats_.incrementCounter('bytes_sent', dataStr.length);
//first, lets get the base64-encoded data
var base64data = util_2.base64Encode(dataStr);
//We can only fit a certain amount in each URL, so we need to split this request
//up into multiple pieces if it doesn't fit in one request.
var dataSegs = util_1.splitStringBySize(base64data, MAX_PAYLOAD_SIZE);
//Enqueue each segment for transmission. We assign each chunk a sequential ID and a total number
//of segments so that we can reassemble the packet on the server.
for (var i = 0; i < dataSegs.length; i++) {
this.scriptTagHolder.enqueueSegment(this.curSegmentNum, dataSegs.length, dataSegs[i]);
this.curSegmentNum++;
}
};
/**
* This is how we notify the server that we're leaving.
* We aren't able to send requests with DHTML on a window close event, but we can
* trigger XHR requests in some browsers (everything but Opera basically).
* @param {!string} id
* @param {!string} pw
*/
BrowserPollConnection.prototype.addDisconnectPingFrame = function (id, pw) {
if (util_3.isNodeSdk())
return;
this.myDisconnFrame = document.createElement('iframe');
var urlParams = {};
urlParams[exports.FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM] = 't';
urlParams[exports.FIREBASE_LONGPOLL_ID_PARAM] = id;
urlParams[exports.FIREBASE_LONGPOLL_PW_PARAM] = pw;
this.myDisconnFrame.src = this.urlFn(urlParams);
this.myDisconnFrame.style.display = 'none';
document.body.appendChild(this.myDisconnFrame);
};
/**
* Used to track the bytes received by this client
* @param {*} args
* @private
*/
BrowserPollConnection.prototype.incrementIncomingBytes_ = function (args) {
// TODO: This is an annoying perf hit just to track the number of incoming bytes. Maybe it should be opt-in.
var bytesReceived = util_2.stringify(args).length;
this.bytesReceived += bytesReceived;
this.stats_.incrementCounter('bytes_received', bytesReceived);
};
return BrowserPollConnection;
}());
exports.BrowserPollConnection = BrowserPollConnection;
/*********************************************************************************************
* A wrapper around an iframe that is used as a long-polling script holder.
* @constructor
*********************************************************************************************/
var FirebaseIFrameScriptHolder = /** @class */ (function () {
/**
* @param commandCB - The callback to be called when control commands are recevied from the server.
* @param onMessageCB - The callback to be triggered when responses arrive from the server.
* @param onDisconnect - The callback to be triggered when this tag holder is closed
* @param urlFn - A function that provides the URL of the endpoint to send data to.
*/
function FirebaseIFrameScriptHolder(commandCB, onMessageCB, onDisconnect, urlFn) {
this.onDisconnect = onDisconnect;
this.urlFn = urlFn;
//We maintain a count of all of the outstanding requests, because if we have too many active at once it can cause
//problems in some browsers.
/**
* @type {CountedSet.<number, number>}
*/
this.outstandingRequests = new CountedSet_1.CountedSet();
//A queue of the pending segments waiting for transmission to the server.
this.pendingSegs = [];
//A serial number. We use this for two things:
// 1) A way to ensure the browser doesn't cache responses to polls
// 2) A way to make the server aware when long-polls arrive in a different order than we started them. The
// server needs to release both polls in this case or it will cause problems in Opera since Opera can only execute
// JSONP code in the order it was added to the iframe.
this.currentSerial = Math.floor(Math.random() * 100000000);
// This gets set to false when we're "closing down" the connection (e.g. we're switching transports but there's still
// incoming data from the server that we're waiting for).
this.sendNewPolls = true;
if (!util_3.isNodeSdk()) {
//Each script holder registers a couple of uniquely named callbacks with the window. These are called from the
//iframes where we put the long-polling script tags. We have two callbacks:
// 1) Command Callback - Triggered for control issues, like starting a connection.
// 2) Message Callback - Triggered when new data arrives.
this.uniqueCallbackIdentifier = util_1.LUIDGenerator();
window[exports.FIREBASE_LONGPOLL_COMMAND_CB_NAME + this.uniqueCallbackIdentifier] = commandCB;
window[exports.FIREBASE_LONGPOLL_DATA_CB_NAME + this.uniqueCallbackIdentifier] = onMessageCB;
//Create an iframe for us to add script tags to.
this.myIFrame = FirebaseIFrameScriptHolder.createIFrame_();
// Set the iframe's contents.
var script = '';
// if we set a javascript url, it's IE and we need to set the document domain. The javascript url is sufficient
// for ie9, but ie8 needs to do it again in the document itself.
if (this.myIFrame.src &&
this.myIFrame.src.substr(0, 'javascript:'.length) === 'javascript:') {
var currentDomain = document.domain;
script = '<script>document.domain="' + currentDomain + '";</script>';
}
var iframeContents = '<html><body>' + script + '</body></html>';
try {
this.myIFrame.doc.open();
this.myIFrame.doc.write(iframeContents);
this.myIFrame.doc.close();
}
catch (e) {
util_1.log('frame writing exception');
if (e.stack) {
util_1.log(e.stack);
}
util_1.log(e);
}
}
else {
this.commandCB = commandCB;
this.onMessageCB = onMessageCB;
}
}
/**
* Each browser has its own funny way to handle iframes. Here we mush them all together into one object that I can
* actually use.
* @private
* @return {Element}
*/
FirebaseIFrameScriptHolder.createIFrame_ = function () {
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
// This is necessary in order to initialize the document inside the iframe
if (document.body) {
document.body.appendChild(iframe);
try {
// If document.domain has been modified in IE, this will throw an error, and we need to set the
// domain of the iframe's document manually. We can do this via a javascript: url as the src attribute
// Also note that we must do this *after* the iframe has been appended to the page. Otherwise it doesn't work.
var a = iframe.contentWindow.document;
if (!a) {
// Apologies for the log-spam, I need to do something to keep closure from optimizing out the assignment above.
util_1.log('No IE domain setting required');
}
}
catch (e) {
var domain = document.domain;
iframe.src =
"javascript:void((function(){document.open();document.domain='" +
domain +
"';document.close();})())";
}
}
else {
// LongPollConnection attempts to delay initialization until the document is ready, so hopefully this
// never gets hit.
throw 'Document body has not initialized. Wait to initialize Firebase until after the document is ready.';
}
// Get the document of the iframe in a browser-specific way.
if (iframe.contentDocument) {
iframe.doc = iframe.contentDocument; // Firefox, Opera, Safari
}
else if (iframe.contentWindow) {
iframe.doc = iframe.contentWindow.document; // Internet Explorer
}
else if (iframe.document) {
iframe.doc = iframe.document; //others?
}
return iframe;
};
/**
* Cancel all outstanding queries and remove the frame.
*/
FirebaseIFrameScriptHolder.prototype.close = function () {
var _this = this;
//Mark this iframe as dead, so no new requests are sent.
this.alive = false;
if (this.myIFrame) {
//We have to actually remove all of the html inside this iframe before removing it from the
//window, or IE will continue loading and executing the script tags we've already added, which
//can lead to some errors being thrown. Setting innerHTML seems to be the easiest way to do this.
this.myIFrame.doc.body.innerHTML = '';
setTimeout(function () {
if (_this.myIFrame !== null) {
document.body.removeChild(_this.myIFrame);
_this.myIFrame = null;
}
}, Math.floor(0));
}
if (util_3.isNodeSdk() && this.myID) {
var urlParams = {};
urlParams[exports.FIREBASE_LONGPOLL_DISCONN_FRAME_PARAM] = 't';
urlParams[exports.FIREBASE_LONGPOLL_ID_PARAM] = this.myID;
urlParams[exports.FIREBASE_LONGPOLL_PW_PARAM] = this.myPW;
var theURL = this.urlFn(urlParams);
FirebaseIFrameScriptHolder.nodeRestRequest(theURL);
}
// Protect from being called recursively.
var onDisconnect = this.onDisconnect;
if (onDisconnect) {
this.onDisconnect = null;
onDisconnect();
}
};
/**
* Actually start the long-polling session by adding the first script tag(s) to the iframe.
* @param {!string} id - The ID of this connection
* @param {!string} pw - The password for this connection
*/
FirebaseIFrameScriptHolder.prototype.startLongPoll = function (id, pw) {
this.myID = id;
this.myPW = pw;
this.alive = true;
//send the initial request. If there are requests queued, make sure that we transmit as many as we are currently able to.
while (this.newRequest_()) { }
};
/**
* This is called any time someone might want a script tag to be added. It adds a script tag when there aren't
* too many outstanding requests and we are still alive.
*
* If there are outstanding packet segments to send, it sends one. If there aren't, it sends a long-poll anyways if
* needed.
*/
FirebaseIFrameScriptHolder.prototype.newRequest_ = function () {
// We keep one outstanding request open all the time to receive data, but if we need to send data
// (pendingSegs.length > 0) then we create a new request to send the data. The server will automatically
// close the old request.
if (this.alive &&
this.sendNewPolls &&
this.outstandingRequests.count() < (this.pendingSegs.length > 0 ? 2 : 1)) {
//construct our url
this.currentSerial++;
var urlParams = {};
urlParams[exports.FIREBASE_LONGPOLL_ID_PARAM] = this.myID;
urlParams[exports.FIREBASE_LONGPOLL_PW_PARAM] = this.myPW;
urlParams[exports.FIREBASE_LONGPOLL_SERIAL_PARAM] = this.currentSerial;
var theURL = this.urlFn(urlParams);
//Now add as much data as we can.
var curDataString = '';
var i = 0;
while (this.pendingSegs.length > 0) {
//first, lets see if the next segment will fit.
var nextSeg = this.pendingSegs[0];
if (nextSeg.d.length + SEG_HEADER_SIZE + curDataString.length <=
MAX_URL_DATA_SIZE) {
//great, the segment will fit. Lets append it.
var theSeg = this.pendingSegs.shift();
curDataString =
curDataString +
'&' +
exports.FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM +
i +
'=' +
theSeg.seg +
'&' +
exports.FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET +
i +
'=' +
theSeg.ts +
'&' +
exports.FIREBASE_LONGPOLL_DATA_PARAM +
i +
'=' +
theSeg.d;
i++;
}
else {
break;
}
}
theURL = theURL + curDataString;
this.addLongPollTag_(theURL, this.currentSerial);
return true;
}
else {
return false;
}
};
/**
* Queue a packet for transmission to the server.
* @param segnum - A sequential id for this packet segment used for reassembly
* @param totalsegs - The total number of segments in this packet
* @param data - The data for this segment.
*/
FirebaseIFrameScriptHolder.prototype.enqueueSegment = function (segnum, totalsegs, data) {
//add this to the queue of segments to send.
this.pendingSegs.push({ seg: segnum, ts: totalsegs, d: data });
//send the data immediately if there isn't already data being transmitted, unless
//startLongPoll hasn't been called yet.
if (this.alive) {
this.newRequest_();
}
};
/**
* Add a script tag for a regular long-poll request.
* @param {!string} url - The URL of the script tag.
* @param {!number} serial - The serial number of the request.
* @private
*/
FirebaseIFrameScriptHolder.prototype.addLongPollTag_ = function (url, serial) {
var _this = this;
//remember that we sent this request.
this.outstandingRequests.add(serial, 1);
var doNewRequest = function () {
_this.outstandingRequests.remove(serial);
_this.newRequest_();
};
// If this request doesn't return on its own accord (by the server sending us some data), we'll
// create a new one after the KEEPALIVE interval to make sure we always keep a fresh request open.
var keepaliveTimeout = setTimeout(doNewRequest, Math.floor(KEEPALIVE_REQUEST_INTERVAL));
var readyStateCB = function () {
// Request completed. Cancel the keepalive.
clearTimeout(keepaliveTimeout);
// Trigger a new request so we can continue receiving data.
doNewRequest();
};
this.addTag(url, readyStateCB);
};
/**
* Add an arbitrary script tag to the iframe.
* @param {!string} url - The URL for the script tag source.
* @param {!function()} loadCB - A callback to be triggered once the script has loaded.
*/
FirebaseIFrameScriptHolder.prototype.addTag = function (url, loadCB) {
var _this = this;
if (util_3.isNodeSdk()) {
this.doNodeLongPoll(url, loadCB);
}
else {
setTimeout(function () {
try {
// if we're already closed, don't add this poll
if (!_this.sendNewPolls)
return;
var newScript_1 = _this.myIFrame.doc.createElement('script');
newScript_1.type = 'text/javascript';
newScript_1.async = true;
newScript_1.src = url;
newScript_1.onload = newScript_1.onreadystatechange = function () {
var rstate = newScript_1.readyState;
if (!rstate || rstate === 'loaded' || rstate === 'complete') {
newScript_1.onload = newScript_1.onreadystatechange = null;
if (newScript_1.parentNode) {
newScript_1.parentNode.removeChild(newScript_1);
}
loadCB();
}
};
newScript_1.onerror = function () {
util_1.log('Long-poll script failed to load: ' + url);
_this.sendNewPolls = false;
_this.close();
};
_this.myIFrame.doc.body.appendChild(newScript_1);
}
catch (e) {
// TODO: we should make this error visible somehow
}
}, Math.floor(1));
}
};
return FirebaseIFrameScriptHolder;
}());
exports.FirebaseIFrameScriptHolder = FirebaseIFrameScriptHolder;
//# sourceMappingURL=BrowserPollConnection.js.map