14521 lines
572 KiB
JavaScript
14521 lines
572 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
var Websocket = require('faye-websocket');
|
|
var util = require('@firebase/util');
|
|
var tslib = require('tslib');
|
|
var logger$1 = require('@firebase/logger');
|
|
var app = require('@firebase/app');
|
|
var component = require('@firebase/component');
|
|
|
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
|
|
var Websocket__default = /*#__PURE__*/_interopDefaultLegacy(Websocket);
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
var PROTOCOL_VERSION = '5';
|
|
var VERSION_PARAM = 'v';
|
|
var TRANSPORT_SESSION_PARAM = 's';
|
|
var REFERER_PARAM = 'r';
|
|
var FORGE_REF = 'f';
|
|
// Matches console.firebase.google.com, firebase-console-*.corp.google.com and
|
|
// firebase.corp.google.com
|
|
var FORGE_DOMAIN_RE = /(console\.firebase|firebase-console-\w+\.corp|firebase\.corp)\.google\.com/;
|
|
var LAST_SESSION_PARAM = 'ls';
|
|
var APPLICATION_ID_PARAM = 'p';
|
|
var APP_CHECK_TOKEN_PARAM = 'ac';
|
|
var WEBSOCKET = 'websocket';
|
|
var LONG_POLLING = 'long_polling';
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* Wraps a DOM Storage object and:
|
|
* - automatically encode objects as JSON strings before storing them to allow us to store arbitrary types.
|
|
* - prefixes names with "firebase:" to avoid collisions with app data.
|
|
*
|
|
* We automatically (see storage.js) create two such wrappers, one for sessionStorage,
|
|
* and one for localStorage.
|
|
*
|
|
*/
|
|
var DOMStorageWrapper = /** @class */ (function () {
|
|
/**
|
|
* @param domStorage_ - The underlying storage object (e.g. localStorage or sessionStorage)
|
|
*/
|
|
function DOMStorageWrapper(domStorage_) {
|
|
this.domStorage_ = domStorage_;
|
|
// Use a prefix to avoid collisions with other stuff saved by the app.
|
|
this.prefix_ = 'firebase:';
|
|
}
|
|
/**
|
|
* @param key - The key to save the value under
|
|
* @param value - The value being stored, or null to remove the key.
|
|
*/
|
|
DOMStorageWrapper.prototype.set = function (key, value) {
|
|
if (value == null) {
|
|
this.domStorage_.removeItem(this.prefixedName_(key));
|
|
}
|
|
else {
|
|
this.domStorage_.setItem(this.prefixedName_(key), util.stringify(value));
|
|
}
|
|
};
|
|
/**
|
|
* @returns The value that was stored under this key, or null
|
|
*/
|
|
DOMStorageWrapper.prototype.get = function (key) {
|
|
var storedVal = this.domStorage_.getItem(this.prefixedName_(key));
|
|
if (storedVal == null) {
|
|
return null;
|
|
}
|
|
else {
|
|
return util.jsonEval(storedVal);
|
|
}
|
|
};
|
|
DOMStorageWrapper.prototype.remove = function (key) {
|
|
this.domStorage_.removeItem(this.prefixedName_(key));
|
|
};
|
|
DOMStorageWrapper.prototype.prefixedName_ = function (name) {
|
|
return this.prefix_ + name;
|
|
};
|
|
DOMStorageWrapper.prototype.toString = function () {
|
|
return this.domStorage_.toString();
|
|
};
|
|
return DOMStorageWrapper;
|
|
}());
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* An in-memory storage implementation that matches the API of DOMStorageWrapper
|
|
* (TODO: create interface for both to implement).
|
|
*/
|
|
var MemoryStorage = /** @class */ (function () {
|
|
function MemoryStorage() {
|
|
this.cache_ = {};
|
|
this.isInMemoryStorage = true;
|
|
}
|
|
MemoryStorage.prototype.set = function (key, value) {
|
|
if (value == null) {
|
|
delete this.cache_[key];
|
|
}
|
|
else {
|
|
this.cache_[key] = value;
|
|
}
|
|
};
|
|
MemoryStorage.prototype.get = function (key) {
|
|
if (util.contains(this.cache_, key)) {
|
|
return this.cache_[key];
|
|
}
|
|
return null;
|
|
};
|
|
MemoryStorage.prototype.remove = function (key) {
|
|
delete this.cache_[key];
|
|
};
|
|
return MemoryStorage;
|
|
}());
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* Helper to create a DOMStorageWrapper or else fall back to MemoryStorage.
|
|
* TODO: Once MemoryStorage and DOMStorageWrapper have a shared interface this method annotation should change
|
|
* to reflect this type
|
|
*
|
|
* @param domStorageName - Name of the underlying storage object
|
|
* (e.g. 'localStorage' or 'sessionStorage').
|
|
* @returns Turning off type information until a common interface is defined.
|
|
*/
|
|
var createStoragefor = function (domStorageName) {
|
|
try {
|
|
// NOTE: just accessing "localStorage" or "window['localStorage']" may throw a security exception,
|
|
// so it must be inside the try/catch.
|
|
if (typeof window !== 'undefined' &&
|
|
typeof window[domStorageName] !== 'undefined') {
|
|
// Need to test cache. Just because it's here doesn't mean it works
|
|
var domStorage = window[domStorageName];
|
|
domStorage.setItem('firebase:sentinel', 'cache');
|
|
domStorage.removeItem('firebase:sentinel');
|
|
return new DOMStorageWrapper(domStorage);
|
|
}
|
|
}
|
|
catch (e) { }
|
|
// Failed to create wrapper. Just return in-memory storage.
|
|
// TODO: log?
|
|
return new MemoryStorage();
|
|
};
|
|
/** A storage object that lasts across sessions */
|
|
var PersistentStorage = createStoragefor('localStorage');
|
|
/** A storage object that only lasts one session */
|
|
var SessionStorage = createStoragefor('sessionStorage');
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
var logClient = new logger$1.Logger('@firebase/database');
|
|
/**
|
|
* Returns a locally-unique ID (generated by just incrementing up from 0 each time its called).
|
|
*/
|
|
var LUIDGenerator = (function () {
|
|
var id = 1;
|
|
return function () {
|
|
return id++;
|
|
};
|
|
})();
|
|
/**
|
|
* Sha1 hash of the input string
|
|
* @param str - The string to hash
|
|
* @returns {!string} The resulting hash
|
|
*/
|
|
var sha1 = function (str) {
|
|
var utf8Bytes = util.stringToByteArray(str);
|
|
var sha1 = new util.Sha1();
|
|
sha1.update(utf8Bytes);
|
|
var sha1Bytes = sha1.digest();
|
|
return util.base64.encodeByteArray(sha1Bytes);
|
|
};
|
|
var buildLogMessage_ = function () {
|
|
var varArgs = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
varArgs[_i] = arguments[_i];
|
|
}
|
|
var message = '';
|
|
for (var i = 0; i < varArgs.length; i++) {
|
|
var arg = varArgs[i];
|
|
if (Array.isArray(arg) ||
|
|
(arg &&
|
|
typeof arg === 'object' &&
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
typeof arg.length === 'number')) {
|
|
message += buildLogMessage_.apply(null, arg);
|
|
}
|
|
else if (typeof arg === 'object') {
|
|
message += util.stringify(arg);
|
|
}
|
|
else {
|
|
message += arg;
|
|
}
|
|
message += ' ';
|
|
}
|
|
return message;
|
|
};
|
|
/**
|
|
* Use this for all debug messages in Firebase.
|
|
*/
|
|
var logger = null;
|
|
/**
|
|
* Flag to check for log availability on first log message
|
|
*/
|
|
var firstLog_ = true;
|
|
/**
|
|
* The implementation of Firebase.enableLogging (defined here to break dependencies)
|
|
* @param logger_ - A flag to turn on logging, or a custom logger
|
|
* @param persistent - Whether or not to persist logging settings across refreshes
|
|
*/
|
|
var enableLogging$1 = function (logger_, persistent) {
|
|
util.assert(!persistent || logger_ === true || logger_ === false, "Can't turn on custom loggers persistently.");
|
|
if (logger_ === true) {
|
|
logClient.logLevel = logger$1.LogLevel.VERBOSE;
|
|
logger = logClient.log.bind(logClient);
|
|
if (persistent) {
|
|
SessionStorage.set('logging_enabled', true);
|
|
}
|
|
}
|
|
else if (typeof logger_ === 'function') {
|
|
logger = logger_;
|
|
}
|
|
else {
|
|
logger = null;
|
|
SessionStorage.remove('logging_enabled');
|
|
}
|
|
};
|
|
var log = function () {
|
|
var varArgs = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
varArgs[_i] = arguments[_i];
|
|
}
|
|
if (firstLog_ === true) {
|
|
firstLog_ = false;
|
|
if (logger === null && SessionStorage.get('logging_enabled') === true) {
|
|
enableLogging$1(true);
|
|
}
|
|
}
|
|
if (logger) {
|
|
var message = buildLogMessage_.apply(null, varArgs);
|
|
logger(message);
|
|
}
|
|
};
|
|
var logWrapper = function (prefix) {
|
|
return function () {
|
|
var varArgs = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
varArgs[_i] = arguments[_i];
|
|
}
|
|
log.apply(void 0, tslib.__spreadArray([prefix], tslib.__read(varArgs)));
|
|
};
|
|
};
|
|
var error = function () {
|
|
var varArgs = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
varArgs[_i] = arguments[_i];
|
|
}
|
|
var message = 'FIREBASE INTERNAL ERROR: ' + buildLogMessage_.apply(void 0, tslib.__spreadArray([], tslib.__read(varArgs)));
|
|
logClient.error(message);
|
|
};
|
|
var fatal = function () {
|
|
var varArgs = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
varArgs[_i] = arguments[_i];
|
|
}
|
|
var message = "FIREBASE FATAL ERROR: " + buildLogMessage_.apply(void 0, tslib.__spreadArray([], tslib.__read(varArgs)));
|
|
logClient.error(message);
|
|
throw new Error(message);
|
|
};
|
|
var warn = function () {
|
|
var varArgs = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
varArgs[_i] = arguments[_i];
|
|
}
|
|
var message = 'FIREBASE WARNING: ' + buildLogMessage_.apply(void 0, tslib.__spreadArray([], tslib.__read(varArgs)));
|
|
logClient.warn(message);
|
|
};
|
|
/**
|
|
* Logs a warning if the containing page uses https. Called when a call to new Firebase
|
|
* does not use https.
|
|
*/
|
|
var warnIfPageIsSecure = function () {
|
|
// Be very careful accessing browser globals. Who knows what may or may not exist.
|
|
if (typeof window !== 'undefined' &&
|
|
window.location &&
|
|
window.location.protocol &&
|
|
window.location.protocol.indexOf('https:') !== -1) {
|
|
warn('Insecure Firebase access from a secure page. ' +
|
|
'Please use https in calls to new Firebase().');
|
|
}
|
|
};
|
|
/**
|
|
* Returns true if data is NaN, or +/- Infinity.
|
|
*/
|
|
var isInvalidJSONNumber = function (data) {
|
|
return (typeof data === 'number' &&
|
|
(data !== data || // NaN
|
|
data === Number.POSITIVE_INFINITY ||
|
|
data === Number.NEGATIVE_INFINITY));
|
|
};
|
|
var executeWhenDOMReady = function (fn) {
|
|
if (util.isNodeSdk() || document.readyState === 'complete') {
|
|
fn();
|
|
}
|
|
else {
|
|
// Modeled after jQuery. Try DOMContentLoaded and onreadystatechange (which
|
|
// fire before onload), but fall back to onload.
|
|
var called_1 = false;
|
|
var wrappedFn_1 = function () {
|
|
if (!document.body) {
|
|
setTimeout(wrappedFn_1, Math.floor(10));
|
|
return;
|
|
}
|
|
if (!called_1) {
|
|
called_1 = true;
|
|
fn();
|
|
}
|
|
};
|
|
if (document.addEventListener) {
|
|
document.addEventListener('DOMContentLoaded', wrappedFn_1, false);
|
|
// fallback to onload.
|
|
window.addEventListener('load', wrappedFn_1, false);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}
|
|
else if (document.attachEvent) {
|
|
// IE.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
document.attachEvent('onreadystatechange', function () {
|
|
if (document.readyState === 'complete') {
|
|
wrappedFn_1();
|
|
}
|
|
});
|
|
// fallback to onload.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
window.attachEvent('onload', wrappedFn_1);
|
|
// jQuery has an extra hack for IE that we could employ (based on
|
|
// http://javascript.nwbox.com/IEContentLoaded/) But it looks really old.
|
|
// I'm hoping we don't need it.
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* Minimum key name. Invalid for actual data, used as a marker to sort before any valid names
|
|
*/
|
|
var MIN_NAME = '[MIN_NAME]';
|
|
/**
|
|
* Maximum key name. Invalid for actual data, used as a marker to sort above any valid names
|
|
*/
|
|
var MAX_NAME = '[MAX_NAME]';
|
|
/**
|
|
* Compares valid Firebase key names, plus min and max name
|
|
*/
|
|
var nameCompare = function (a, b) {
|
|
if (a === b) {
|
|
return 0;
|
|
}
|
|
else if (a === MIN_NAME || b === MAX_NAME) {
|
|
return -1;
|
|
}
|
|
else if (b === MIN_NAME || a === MAX_NAME) {
|
|
return 1;
|
|
}
|
|
else {
|
|
var aAsInt = tryParseInt(a), bAsInt = tryParseInt(b);
|
|
if (aAsInt !== null) {
|
|
if (bAsInt !== null) {
|
|
return aAsInt - bAsInt === 0 ? a.length - b.length : aAsInt - bAsInt;
|
|
}
|
|
else {
|
|
return -1;
|
|
}
|
|
}
|
|
else if (bAsInt !== null) {
|
|
return 1;
|
|
}
|
|
else {
|
|
return a < b ? -1 : 1;
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* @returns {!number} comparison result.
|
|
*/
|
|
var stringCompare = function (a, b) {
|
|
if (a === b) {
|
|
return 0;
|
|
}
|
|
else if (a < b) {
|
|
return -1;
|
|
}
|
|
else {
|
|
return 1;
|
|
}
|
|
};
|
|
var requireKey = function (key, obj) {
|
|
if (obj && key in obj) {
|
|
return obj[key];
|
|
}
|
|
else {
|
|
throw new Error('Missing required key (' + key + ') in object: ' + util.stringify(obj));
|
|
}
|
|
};
|
|
var ObjectToUniqueKey = function (obj) {
|
|
if (typeof obj !== 'object' || obj === null) {
|
|
return util.stringify(obj);
|
|
}
|
|
var keys = [];
|
|
// eslint-disable-next-line guard-for-in
|
|
for (var k in obj) {
|
|
keys.push(k);
|
|
}
|
|
// Export as json, but with the keys sorted.
|
|
keys.sort();
|
|
var key = '{';
|
|
for (var i = 0; i < keys.length; i++) {
|
|
if (i !== 0) {
|
|
key += ',';
|
|
}
|
|
key += util.stringify(keys[i]);
|
|
key += ':';
|
|
key += ObjectToUniqueKey(obj[keys[i]]);
|
|
}
|
|
key += '}';
|
|
return key;
|
|
};
|
|
/**
|
|
* Splits a string into a number of smaller segments of maximum size
|
|
* @param str - The string
|
|
* @param segsize - The maximum number of chars in the string.
|
|
* @returns The string, split into appropriately-sized chunks
|
|
*/
|
|
var splitStringBySize = function (str, segsize) {
|
|
var len = str.length;
|
|
if (len <= segsize) {
|
|
return [str];
|
|
}
|
|
var dataSegs = [];
|
|
for (var c = 0; c < len; c += segsize) {
|
|
if (c + segsize > len) {
|
|
dataSegs.push(str.substring(c, len));
|
|
}
|
|
else {
|
|
dataSegs.push(str.substring(c, c + segsize));
|
|
}
|
|
}
|
|
return dataSegs;
|
|
};
|
|
/**
|
|
* Apply a function to each (key, value) pair in an object or
|
|
* apply a function to each (index, value) pair in an array
|
|
* @param obj - The object or array to iterate over
|
|
* @param fn - The function to apply
|
|
*/
|
|
function each(obj, fn) {
|
|
for (var key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
fn(key, obj[key]);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Borrowed from http://hg.secondlife.com/llsd/src/tip/js/typedarray.js (MIT License)
|
|
* I made one modification at the end and removed the NaN / Infinity
|
|
* handling (since it seemed broken [caused an overflow] and we don't need it). See MJL comments.
|
|
* @param v - A double
|
|
*
|
|
*/
|
|
var doubleToIEEE754String = function (v) {
|
|
util.assert(!isInvalidJSONNumber(v), 'Invalid JSON number'); // MJL
|
|
var ebits = 11, fbits = 52;
|
|
var bias = (1 << (ebits - 1)) - 1;
|
|
var s, e, f, ln, i;
|
|
// Compute sign, exponent, fraction
|
|
// Skip NaN / Infinity handling --MJL.
|
|
if (v === 0) {
|
|
e = 0;
|
|
f = 0;
|
|
s = 1 / v === -Infinity ? 1 : 0;
|
|
}
|
|
else {
|
|
s = v < 0;
|
|
v = Math.abs(v);
|
|
if (v >= Math.pow(2, 1 - bias)) {
|
|
// Normalized
|
|
ln = Math.min(Math.floor(Math.log(v) / Math.LN2), bias);
|
|
e = ln + bias;
|
|
f = Math.round(v * Math.pow(2, fbits - ln) - Math.pow(2, fbits));
|
|
}
|
|
else {
|
|
// Denormalized
|
|
e = 0;
|
|
f = Math.round(v / Math.pow(2, 1 - bias - fbits));
|
|
}
|
|
}
|
|
// Pack sign, exponent, fraction
|
|
var bits = [];
|
|
for (i = fbits; i; i -= 1) {
|
|
bits.push(f % 2 ? 1 : 0);
|
|
f = Math.floor(f / 2);
|
|
}
|
|
for (i = ebits; i; i -= 1) {
|
|
bits.push(e % 2 ? 1 : 0);
|
|
e = Math.floor(e / 2);
|
|
}
|
|
bits.push(s ? 1 : 0);
|
|
bits.reverse();
|
|
var str = bits.join('');
|
|
// Return the data as a hex string. --MJL
|
|
var hexByteString = '';
|
|
for (i = 0; i < 64; i += 8) {
|
|
var hexByte = parseInt(str.substr(i, 8), 2).toString(16);
|
|
if (hexByte.length === 1) {
|
|
hexByte = '0' + hexByte;
|
|
}
|
|
hexByteString = hexByteString + hexByte;
|
|
}
|
|
return hexByteString.toLowerCase();
|
|
};
|
|
/**
|
|
* Used to detect if we're in a Chrome content script (which executes in an
|
|
* isolated environment where long-polling doesn't work).
|
|
*/
|
|
var isChromeExtensionContentScript = function () {
|
|
return !!(typeof window === 'object' &&
|
|
window['chrome'] &&
|
|
window['chrome']['extension'] &&
|
|
!/^chrome/.test(window.location.href));
|
|
};
|
|
/**
|
|
* Used to detect if we're in a Windows 8 Store app.
|
|
*/
|
|
var isWindowsStoreApp = function () {
|
|
// Check for the presence of a couple WinRT globals
|
|
return typeof Windows === 'object' && typeof Windows.UI === 'object';
|
|
};
|
|
/**
|
|
* Converts a server error code to a Javascript Error
|
|
*/
|
|
function errorForServerCode(code, query) {
|
|
var reason = 'Unknown Error';
|
|
if (code === 'too_big') {
|
|
reason =
|
|
'The data requested exceeds the maximum size ' +
|
|
'that can be accessed with a single request.';
|
|
}
|
|
else if (code === 'permission_denied') {
|
|
reason = "Client doesn't have permission to access the desired data.";
|
|
}
|
|
else if (code === 'unavailable') {
|
|
reason = 'The service is unavailable';
|
|
}
|
|
var error = new Error(code + ' at ' + query._path.toString() + ': ' + reason);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
error.code = code.toUpperCase();
|
|
return error;
|
|
}
|
|
/**
|
|
* Used to test for integer-looking strings
|
|
*/
|
|
var INTEGER_REGEXP_ = new RegExp('^-?(0*)\\d{1,10}$');
|
|
/**
|
|
* For use in keys, the minimum possible 32-bit integer.
|
|
*/
|
|
var INTEGER_32_MIN = -2147483648;
|
|
/**
|
|
* For use in kyes, the maximum possible 32-bit integer.
|
|
*/
|
|
var INTEGER_32_MAX = 2147483647;
|
|
/**
|
|
* If the string contains a 32-bit integer, return it. Else return null.
|
|
*/
|
|
var tryParseInt = function (str) {
|
|
if (INTEGER_REGEXP_.test(str)) {
|
|
var intVal = Number(str);
|
|
if (intVal >= INTEGER_32_MIN && intVal <= INTEGER_32_MAX) {
|
|
return intVal;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
/**
|
|
* Helper to run some code but catch any exceptions and re-throw them later.
|
|
* Useful for preventing user callbacks from breaking internal code.
|
|
*
|
|
* Re-throwing the exception from a setTimeout is a little evil, but it's very
|
|
* convenient (we don't have to try to figure out when is a safe point to
|
|
* re-throw it), and the behavior seems reasonable:
|
|
*
|
|
* * If you aren't pausing on exceptions, you get an error in the console with
|
|
* the correct stack trace.
|
|
* * If you're pausing on all exceptions, the debugger will pause on your
|
|
* exception and then again when we rethrow it.
|
|
* * If you're only pausing on uncaught exceptions, the debugger will only pause
|
|
* on us re-throwing it.
|
|
*
|
|
* @param fn - The code to guard.
|
|
*/
|
|
var exceptionGuard = function (fn) {
|
|
try {
|
|
fn();
|
|
}
|
|
catch (e) {
|
|
// Re-throw exception when it's safe.
|
|
setTimeout(function () {
|
|
// It used to be that "throw e" would result in a good console error with
|
|
// relevant context, but as of Chrome 39, you just get the firebase.js
|
|
// file/line number where we re-throw it, which is useless. So we log
|
|
// e.stack explicitly.
|
|
var stack = e.stack || '';
|
|
warn('Exception was thrown by user callback.', stack);
|
|
throw e;
|
|
}, Math.floor(0));
|
|
}
|
|
};
|
|
/**
|
|
* @returns {boolean} true if we think we're currently being crawled.
|
|
*/
|
|
var beingCrawled = function () {
|
|
var userAgent = (typeof window === 'object' &&
|
|
window['navigator'] &&
|
|
window['navigator']['userAgent']) ||
|
|
'';
|
|
// For now we whitelist the most popular crawlers. We should refine this to be the set of crawlers we
|
|
// believe to support JavaScript/AJAX rendering.
|
|
// NOTE: Google Webmaster Tools doesn't really belong, but their "This is how a visitor to your website
|
|
// would have seen the page" is flaky if we don't treat it as a crawler.
|
|
return (userAgent.search(/googlebot|google webmaster tools|bingbot|yahoo! slurp|baiduspider|yandexbot|duckduckbot/i) >= 0);
|
|
};
|
|
/**
|
|
* Same as setTimeout() except on Node.JS it will /not/ prevent the process from exiting.
|
|
*
|
|
* It is removed with clearTimeout() as normal.
|
|
*
|
|
* @param fn - Function to run.
|
|
* @param time - Milliseconds to wait before running.
|
|
* @returns The setTimeout() return value.
|
|
*/
|
|
var setTimeoutNonBlocking = function (fn, time) {
|
|
var timeout = setTimeout(fn, time);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if (typeof timeout === 'object' && timeout['unref']) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
timeout['unref']();
|
|
}
|
|
return timeout;
|
|
};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* A class that holds metadata about a Repo object
|
|
*/
|
|
var RepoInfo = /** @class */ (function () {
|
|
/**
|
|
* @param host - Hostname portion of the url for the repo
|
|
* @param secure - Whether or not this repo is accessed over ssl
|
|
* @param namespace - The namespace represented by the repo
|
|
* @param webSocketOnly - Whether to prefer websockets over all other transports (used by Nest).
|
|
* @param nodeAdmin - Whether this instance uses Admin SDK credentials
|
|
* @param persistenceKey - Override the default session persistence storage key
|
|
*/
|
|
function RepoInfo(host, secure, namespace, webSocketOnly, nodeAdmin, persistenceKey, includeNamespaceInQueryParams) {
|
|
if (nodeAdmin === void 0) { nodeAdmin = false; }
|
|
if (persistenceKey === void 0) { persistenceKey = ''; }
|
|
if (includeNamespaceInQueryParams === void 0) { includeNamespaceInQueryParams = false; }
|
|
this.secure = secure;
|
|
this.namespace = namespace;
|
|
this.webSocketOnly = webSocketOnly;
|
|
this.nodeAdmin = nodeAdmin;
|
|
this.persistenceKey = persistenceKey;
|
|
this.includeNamespaceInQueryParams = includeNamespaceInQueryParams;
|
|
this._host = host.toLowerCase();
|
|
this._domain = this._host.substr(this._host.indexOf('.') + 1);
|
|
this.internalHost =
|
|
PersistentStorage.get('host:' + host) || this._host;
|
|
}
|
|
RepoInfo.prototype.isCacheableHost = function () {
|
|
return this.internalHost.substr(0, 2) === 's-';
|
|
};
|
|
RepoInfo.prototype.isCustomHost = function () {
|
|
return (this._domain !== 'firebaseio.com' &&
|
|
this._domain !== 'firebaseio-demo.com');
|
|
};
|
|
Object.defineProperty(RepoInfo.prototype, "host", {
|
|
get: function () {
|
|
return this._host;
|
|
},
|
|
set: function (newHost) {
|
|
if (newHost !== this.internalHost) {
|
|
this.internalHost = newHost;
|
|
if (this.isCacheableHost()) {
|
|
PersistentStorage.set('host:' + this._host, this.internalHost);
|
|
}
|
|
}
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
RepoInfo.prototype.toString = function () {
|
|
var str = this.toURLString();
|
|
if (this.persistenceKey) {
|
|
str += '<' + this.persistenceKey + '>';
|
|
}
|
|
return str;
|
|
};
|
|
RepoInfo.prototype.toURLString = function () {
|
|
var protocol = this.secure ? 'https://' : 'http://';
|
|
var query = this.includeNamespaceInQueryParams
|
|
? "?ns=" + this.namespace
|
|
: '';
|
|
return "" + protocol + this.host + "/" + query;
|
|
};
|
|
return RepoInfo;
|
|
}());
|
|
function repoInfoNeedsQueryParam(repoInfo) {
|
|
return (repoInfo.host !== repoInfo.internalHost ||
|
|
repoInfo.isCustomHost() ||
|
|
repoInfo.includeNamespaceInQueryParams);
|
|
}
|
|
/**
|
|
* Returns the websocket URL for this repo
|
|
* @param repoInfo - RepoInfo object
|
|
* @param type - of connection
|
|
* @param params - list
|
|
* @returns The URL for this repo
|
|
*/
|
|
function repoInfoConnectionURL(repoInfo, type, params) {
|
|
util.assert(typeof type === 'string', 'typeof type must == string');
|
|
util.assert(typeof params === 'object', 'typeof params must == object');
|
|
var connURL;
|
|
if (type === WEBSOCKET) {
|
|
connURL =
|
|
(repoInfo.secure ? 'wss://' : 'ws://') + repoInfo.internalHost + '/.ws?';
|
|
}
|
|
else if (type === LONG_POLLING) {
|
|
connURL =
|
|
(repoInfo.secure ? 'https://' : 'http://') +
|
|
repoInfo.internalHost +
|
|
'/.lp?';
|
|
}
|
|
else {
|
|
throw new Error('Unknown connection type: ' + type);
|
|
}
|
|
if (repoInfoNeedsQueryParam(repoInfo)) {
|
|
params['ns'] = repoInfo.namespace;
|
|
}
|
|
var pairs = [];
|
|
each(params, function (key, value) {
|
|
pairs.push(key + '=' + value);
|
|
});
|
|
return connURL + pairs.join('&');
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* Tracks a collection of stats.
|
|
*/
|
|
var StatsCollection = /** @class */ (function () {
|
|
function StatsCollection() {
|
|
this.counters_ = {};
|
|
}
|
|
StatsCollection.prototype.incrementCounter = function (name, amount) {
|
|
if (amount === void 0) { amount = 1; }
|
|
if (!util.contains(this.counters_, name)) {
|
|
this.counters_[name] = 0;
|
|
}
|
|
this.counters_[name] += amount;
|
|
};
|
|
StatsCollection.prototype.get = function () {
|
|
return util.deepCopy(this.counters_);
|
|
};
|
|
return StatsCollection;
|
|
}());
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
var collections = {};
|
|
var reporters = {};
|
|
function statsManagerGetCollection(repoInfo) {
|
|
var hashString = repoInfo.toString();
|
|
if (!collections[hashString]) {
|
|
collections[hashString] = new StatsCollection();
|
|
}
|
|
return collections[hashString];
|
|
}
|
|
function statsManagerGetOrCreateReporter(repoInfo, creatorFunction) {
|
|
var hashString = repoInfo.toString();
|
|
if (!reporters[hashString]) {
|
|
reporters[hashString] = creatorFunction();
|
|
}
|
|
return reporters[hashString];
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2019 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/** The semver (www.semver.org) version of the SDK. */
|
|
var SDK_VERSION = '';
|
|
/**
|
|
* SDK_VERSION should be set before any database instance is created
|
|
* @internal
|
|
*/
|
|
function setSDKVersion(version) {
|
|
SDK_VERSION = version;
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
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;
|
|
}
|
|
/**
|
|
* Create a new websocket connection with the given callbacks.
|
|
*/
|
|
var WebSocketConnection = /** @class */ (function () {
|
|
/**
|
|
* @param connId identifier for this transport
|
|
* @param repoInfo The info for the websocket endpoint.
|
|
* @param applicationId The Firebase App ID for this project.
|
|
* @param appCheckToken The App Check Token for this client.
|
|
* @param authToken The Auth Token for this client.
|
|
* @param transportSessionId Optional transportSessionId if this is connecting
|
|
* to an existing transport session
|
|
* @param lastSessionId Optional lastSessionId if there was a previous
|
|
* connection
|
|
*/
|
|
function WebSocketConnection(connId, repoInfo, applicationId, appCheckToken, authToken, transportSessionId, lastSessionId) {
|
|
this.connId = connId;
|
|
this.applicationId = applicationId;
|
|
this.appCheckToken = appCheckToken;
|
|
this.authToken = authToken;
|
|
this.keepaliveTimer = null;
|
|
this.frames = null;
|
|
this.totalFrames = 0;
|
|
this.bytesSent = 0;
|
|
this.bytesReceived = 0;
|
|
this.log_ = logWrapper(this.connId);
|
|
this.stats_ = statsManagerGetCollection(repoInfo);
|
|
this.connURL = WebSocketConnection.connectionURL_(repoInfo, transportSessionId, lastSessionId, appCheckToken);
|
|
this.nodeAdmin = repoInfo.nodeAdmin;
|
|
}
|
|
/**
|
|
* @param repoInfo - The info for the websocket endpoint.
|
|
* @param transportSessionId - Optional transportSessionId if this is connecting to an existing transport
|
|
* session
|
|
* @param lastSessionId - Optional lastSessionId if there was a previous connection
|
|
* @returns connection url
|
|
*/
|
|
WebSocketConnection.connectionURL_ = function (repoInfo, transportSessionId, lastSessionId, appCheckToken) {
|
|
var urlParams = {};
|
|
urlParams[VERSION_PARAM] = PROTOCOL_VERSION;
|
|
if (!util.isNodeSdk() &&
|
|
typeof location !== 'undefined' &&
|
|
location.hostname &&
|
|
FORGE_DOMAIN_RE.test(location.hostname)) {
|
|
urlParams[REFERER_PARAM] = FORGE_REF;
|
|
}
|
|
if (transportSessionId) {
|
|
urlParams[TRANSPORT_SESSION_PARAM] = transportSessionId;
|
|
}
|
|
if (lastSessionId) {
|
|
urlParams[LAST_SESSION_PARAM] = lastSessionId;
|
|
}
|
|
if (appCheckToken) {
|
|
urlParams[APP_CHECK_TOKEN_PARAM] = appCheckToken;
|
|
}
|
|
return repoInfoConnectionURL(repoInfo, 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.
|
|
PersistentStorage.set('previous_websocket_failure', true);
|
|
try {
|
|
if (util.isNodeSdk()) {
|
|
var device = this.nodeAdmin ? 'AdminNode' : 'Node';
|
|
// UA Format: Firebase/<wire_protocol>/<sdk_version>/<platform>/<device>
|
|
var options = {
|
|
headers: {
|
|
'User-Agent': "Firebase/" + PROTOCOL_VERSION + "/" + SDK_VERSION + "/" + process.platform + "/" + device,
|
|
'X-Firebase-GMPID': this.applicationId || ''
|
|
}
|
|
};
|
|
// If using Node with admin creds, AppCheck-related checks are unnecessary.
|
|
// Note that we send the credentials here even if they aren't admin credentials, which is
|
|
// not a problem.
|
|
// Note that this header is just used to bypass appcheck, and the token should still be sent
|
|
// through the websocket connection once it is established.
|
|
if (this.authToken) {
|
|
options.headers['Authorization'] = "Bearer " + this.authToken;
|
|
}
|
|
if (this.appCheckToken) {
|
|
options.headers['X-Firebase-AppCheck'] = this.appCheckToken;
|
|
}
|
|
// 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 {
|
|
var options = {
|
|
headers: {
|
|
'X-Firebase-GMPID': this.applicationId || '',
|
|
'X-Firebase-AppCheck': this.appCheckToken || ''
|
|
}
|
|
};
|
|
this.mySock = new WebSocketImpl(this.connURL, [], options);
|
|
}
|
|
}
|
|
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.');
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
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.
|
|
*/
|
|
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 (PersistentStorage.isInMemoryStorage ||
|
|
PersistentStorage.get('previous_websocket_failure') === true);
|
|
};
|
|
WebSocketConnection.prototype.markConnectionHealthy = function () {
|
|
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.jsonEval(fullMess);
|
|
//handle the message
|
|
this.onMessage(jsonMess);
|
|
}
|
|
};
|
|
/**
|
|
* @param frameCount - The number of frames we are expecting from the server
|
|
*/
|
|
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
|
|
* @returns Any remaining data to be process, or null if there is none
|
|
*/
|
|
WebSocketConnection.prototype.extractFrameCount_ = function (data) {
|
|
util.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 data - The JSON object to transmit
|
|
*/
|
|
WebSocketConnection.prototype.send = function (data) {
|
|
this.resetKeepAlive();
|
|
var dataStr = util.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 = 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();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}, Math.floor(WEBSOCKET_KEEPALIVE_INTERVAL));
|
|
};
|
|
/**
|
|
* Send a string over the websocket.
|
|
*
|
|
* @param str - String to send.
|
|
*/
|
|
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."
|
|
*/
|
|
WebSocketConnection.responsesRequiredToBeHealthy = 2;
|
|
/**
|
|
* Time to wait for the connection te become healthy before giving up.
|
|
*/
|
|
WebSocketConnection.healthyTimeout = 30000;
|
|
return WebSocketConnection;
|
|
}());
|
|
|
|
var name = "@firebase/database";
|
|
var version = "0.12.4";
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* Abstraction around AppCheck's token fetching capabilities.
|
|
*/
|
|
var AppCheckTokenProvider = /** @class */ (function () {
|
|
function AppCheckTokenProvider(appName_, appCheckProvider) {
|
|
var _this = this;
|
|
this.appName_ = appName_;
|
|
this.appCheckProvider = appCheckProvider;
|
|
this.appCheck = appCheckProvider === null || appCheckProvider === void 0 ? void 0 : appCheckProvider.getImmediate({ optional: true });
|
|
if (!this.appCheck) {
|
|
appCheckProvider === null || appCheckProvider === void 0 ? void 0 : appCheckProvider.get().then(function (appCheck) { return (_this.appCheck = appCheck); });
|
|
}
|
|
}
|
|
AppCheckTokenProvider.prototype.getToken = function (forceRefresh) {
|
|
var _this = this;
|
|
if (!this.appCheck) {
|
|
return new Promise(function (resolve, reject) {
|
|
// Support delayed initialization of FirebaseAppCheck. This allows our
|
|
// customers to initialize the RTDB SDK before initializing Firebase
|
|
// AppCheck and ensures that all requests are authenticated if a token
|
|
// becomes available before the timoeout below expires.
|
|
setTimeout(function () {
|
|
if (_this.appCheck) {
|
|
_this.getToken(forceRefresh).then(resolve, reject);
|
|
}
|
|
else {
|
|
resolve(null);
|
|
}
|
|
}, 0);
|
|
});
|
|
}
|
|
return this.appCheck.getToken(forceRefresh);
|
|
};
|
|
AppCheckTokenProvider.prototype.addTokenChangeListener = function (listener) {
|
|
var _a;
|
|
(_a = this.appCheckProvider) === null || _a === void 0 ? void 0 : _a.get().then(function (appCheck) { return appCheck.addTokenListener(listener); });
|
|
};
|
|
AppCheckTokenProvider.prototype.notifyForInvalidToken = function () {
|
|
warn("Provided AppCheck credentials for the app named \"" + this.appName_ + "\" " +
|
|
'are invalid. This usually indicates your app was not initialized correctly.');
|
|
};
|
|
return AppCheckTokenProvider;
|
|
}());
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* Abstraction around FirebaseApp's token fetching capabilities.
|
|
*/
|
|
var FirebaseAuthTokenProvider = /** @class */ (function () {
|
|
function FirebaseAuthTokenProvider(appName_, firebaseOptions_, authProvider_) {
|
|
var _this = this;
|
|
this.appName_ = appName_;
|
|
this.firebaseOptions_ = firebaseOptions_;
|
|
this.authProvider_ = authProvider_;
|
|
this.auth_ = null;
|
|
this.auth_ = authProvider_.getImmediate({ optional: true });
|
|
if (!this.auth_) {
|
|
authProvider_.onInit(function (auth) { return (_this.auth_ = auth); });
|
|
}
|
|
}
|
|
FirebaseAuthTokenProvider.prototype.getToken = function (forceRefresh) {
|
|
var _this = this;
|
|
if (!this.auth_) {
|
|
return new Promise(function (resolve, reject) {
|
|
// Support delayed initialization of FirebaseAuth. This allows our
|
|
// customers to initialize the RTDB SDK before initializing Firebase
|
|
// Auth and ensures that all requests are authenticated if a token
|
|
// becomes available before the timoeout below expires.
|
|
setTimeout(function () {
|
|
if (_this.auth_) {
|
|
_this.getToken(forceRefresh).then(resolve, reject);
|
|
}
|
|
else {
|
|
resolve(null);
|
|
}
|
|
}, 0);
|
|
});
|
|
}
|
|
return this.auth_.getToken(forceRefresh).catch(function (error) {
|
|
// TODO: Need to figure out all the cases this is raised and whether
|
|
// this makes sense.
|
|
if (error && error.code === 'auth/token-not-initialized') {
|
|
log('Got auth/token-not-initialized error. Treating as null token.');
|
|
return null;
|
|
}
|
|
else {
|
|
return Promise.reject(error);
|
|
}
|
|
});
|
|
};
|
|
FirebaseAuthTokenProvider.prototype.addTokenChangeListener = function (listener) {
|
|
// TODO: We might want to wrap the listener and call it with no args to
|
|
// avoid a leaky abstraction, but that makes removing the listener harder.
|
|
if (this.auth_) {
|
|
this.auth_.addAuthTokenListener(listener);
|
|
}
|
|
else {
|
|
this.authProvider_
|
|
.get()
|
|
.then(function (auth) { return auth.addAuthTokenListener(listener); });
|
|
}
|
|
};
|
|
FirebaseAuthTokenProvider.prototype.removeTokenChangeListener = function (listener) {
|
|
this.authProvider_
|
|
.get()
|
|
.then(function (auth) { return auth.removeAuthTokenListener(listener); });
|
|
};
|
|
FirebaseAuthTokenProvider.prototype.notifyForInvalidToken = function () {
|
|
var errorMessage = 'Provided authentication credentials for the app named "' +
|
|
this.appName_ +
|
|
'" are invalid. This usually indicates your app was not ' +
|
|
'initialized correctly. ';
|
|
if ('credential' in this.firebaseOptions_) {
|
|
errorMessage +=
|
|
'Make sure the "credential" property provided to initializeApp() ' +
|
|
'is authorized to access the specified "databaseURL" and is from the correct ' +
|
|
'project.';
|
|
}
|
|
else if ('serviceAccount' in this.firebaseOptions_) {
|
|
errorMessage +=
|
|
'Make sure the "serviceAccount" property provided to initializeApp() ' +
|
|
'is authorized to access the specified "databaseURL" and is from the correct ' +
|
|
'project.';
|
|
}
|
|
else {
|
|
errorMessage +=
|
|
'Make sure the "apiKey" and "databaseURL" properties provided to ' +
|
|
'initializeApp() match the values provided for your app at ' +
|
|
'https://console.firebase.google.com/.';
|
|
}
|
|
warn(errorMessage);
|
|
};
|
|
return FirebaseAuthTokenProvider;
|
|
}());
|
|
/* AuthTokenProvider that supplies a constant token. Used by Admin SDK or mockUserToken with emulators. */
|
|
var EmulatorTokenProvider = /** @class */ (function () {
|
|
function EmulatorTokenProvider(accessToken) {
|
|
this.accessToken = accessToken;
|
|
}
|
|
EmulatorTokenProvider.prototype.getToken = function (forceRefresh) {
|
|
return Promise.resolve({
|
|
accessToken: this.accessToken
|
|
});
|
|
};
|
|
EmulatorTokenProvider.prototype.addTokenChangeListener = function (listener) {
|
|
// Invoke the listener immediately to match the behavior in Firebase Auth
|
|
// (see packages/auth/src/auth.js#L1807)
|
|
listener(this.accessToken);
|
|
};
|
|
EmulatorTokenProvider.prototype.removeTokenChangeListener = function (listener) { };
|
|
EmulatorTokenProvider.prototype.notifyForInvalidToken = function () { };
|
|
/** A string that is treated as an admin access token by the RTDB emulator. Used by Admin SDK. */
|
|
EmulatorTokenProvider.OWNER = 'owner';
|
|
return EmulatorTokenProvider;
|
|
}());
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* This class ensures the packets from the server arrive in order
|
|
* This class takes data from the server and ensures it gets passed into the callbacks in order.
|
|
*/
|
|
var PacketReceiver = /** @class */ (function () {
|
|
/**
|
|
* @param onMessage_
|
|
*/
|
|
function PacketReceiver(onMessage_) {
|
|
this.onMessage_ = onMessage_;
|
|
this.pendingResponses = [];
|
|
this.currentResponseNum = 0;
|
|
this.closeAfterResponse = -1;
|
|
this.onClose = null;
|
|
}
|
|
PacketReceiver.prototype.closeAfter = function (responseNum, callback) {
|
|
this.closeAfterResponse = responseNum;
|
|
this.onClose = callback;
|
|
if (this.closeAfterResponse < this.currentResponseNum) {
|
|
this.onClose();
|
|
this.onClose = null;
|
|
}
|
|
};
|
|
/**
|
|
* Each message from the server comes with a response number, and an array of data. The responseNumber
|
|
* allows us to ensure that we process them in the right order, since we can't be guaranteed that all
|
|
* browsers will respond in the same order as the requests we sent
|
|
*/
|
|
PacketReceiver.prototype.handleResponse = function (requestNum, data) {
|
|
var _this = this;
|
|
this.pendingResponses[requestNum] = data;
|
|
var _loop_1 = function () {
|
|
var toProcess = this_1.pendingResponses[this_1.currentResponseNum];
|
|
delete this_1.pendingResponses[this_1.currentResponseNum];
|
|
var _loop_2 = function (i) {
|
|
if (toProcess[i]) {
|
|
exceptionGuard(function () {
|
|
_this.onMessage_(toProcess[i]);
|
|
});
|
|
}
|
|
};
|
|
for (var i = 0; i < toProcess.length; ++i) {
|
|
_loop_2(i);
|
|
}
|
|
if (this_1.currentResponseNum === this_1.closeAfterResponse) {
|
|
if (this_1.onClose) {
|
|
this_1.onClose();
|
|
this_1.onClose = null;
|
|
}
|
|
return "break";
|
|
}
|
|
this_1.currentResponseNum++;
|
|
};
|
|
var this_1 = this;
|
|
while (this.pendingResponses[this.currentResponseNum]) {
|
|
var state_1 = _loop_1();
|
|
if (state_1 === "break")
|
|
break;
|
|
}
|
|
};
|
|
return PacketReceiver;
|
|
}());
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
// URL query parameters associated with longpolling
|
|
var FIREBASE_LONGPOLL_START_PARAM = 'start';
|
|
var FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close';
|
|
var FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand';
|
|
var FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB';
|
|
var FIREBASE_LONGPOLL_ID_PARAM = 'id';
|
|
var FIREBASE_LONGPOLL_PW_PARAM = 'pw';
|
|
var FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser';
|
|
var FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb';
|
|
var FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg';
|
|
var FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts';
|
|
var FIREBASE_LONGPOLL_DATA_PARAM = 'd';
|
|
var 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.
|
|
*/
|
|
var KEEPALIVE_REQUEST_INTERVAL = 25000;
|
|
/**
|
|
* How long to wait before aborting a long-polling connection attempt.
|
|
*/
|
|
var LP_CONNECT_TIMEOUT = 30000;
|
|
/**
|
|
* This class manages a single long-polling connection.
|
|
*/
|
|
var BrowserPollConnection = /** @class */ (function () {
|
|
/**
|
|
* @param connId An identifier for this connection, used for logging
|
|
* @param repoInfo The info for the endpoint to send data to.
|
|
* @param applicationId The Firebase App ID for this project.
|
|
* @param appCheckToken The AppCheck token for this client.
|
|
* @param authToken The AuthToken to use for this connection.
|
|
* @param transportSessionId Optional transportSessionid if we are
|
|
* reconnecting for an existing transport session
|
|
* @param lastSessionId Optional lastSessionId if the PersistentConnection has
|
|
* already created a connection previously
|
|
*/
|
|
function BrowserPollConnection(connId, repoInfo, applicationId, appCheckToken, authToken, transportSessionId, lastSessionId) {
|
|
var _this = this;
|
|
this.connId = connId;
|
|
this.repoInfo = repoInfo;
|
|
this.applicationId = applicationId;
|
|
this.appCheckToken = appCheckToken;
|
|
this.authToken = authToken;
|
|
this.transportSessionId = transportSessionId;
|
|
this.lastSessionId = lastSessionId;
|
|
this.bytesSent = 0;
|
|
this.bytesReceived = 0;
|
|
this.everConnected_ = false;
|
|
this.log_ = logWrapper(connId);
|
|
this.stats_ = statsManagerGetCollection(repoInfo);
|
|
this.urlFn = function (params) {
|
|
// Always add the token if we have one.
|
|
if (_this.appCheckToken) {
|
|
params[APP_CHECK_TOKEN_PARAM] = _this.appCheckToken;
|
|
}
|
|
return repoInfoConnectionURL(repoInfo, LONG_POLLING, params);
|
|
};
|
|
}
|
|
/**
|
|
* @param onMessage - Callback when messages arrive
|
|
* @param onDisconnect - Callback with connection lost.
|
|
*/
|
|
BrowserPollConnection.prototype.open = function (onMessage, onDisconnect) {
|
|
var _this = this;
|
|
this.curSegmentNum = 0;
|
|
this.onDisconnect_ = onDisconnect;
|
|
this.myPacketOrderer = new 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;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}, Math.floor(LP_CONNECT_TIMEOUT));
|
|
// Ensure we delay the creation of the iframe until the DOM is loaded.
|
|
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 _a = tslib.__read(args, 5), command = _a[0], arg1 = _a[1], arg2 = _a[2]; _a[3]; _a[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 === FIREBASE_LONGPOLL_START_PARAM) {
|
|
_this.id = arg1;
|
|
_this.password = arg2;
|
|
}
|
|
else if (command === 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 _a = tslib.__read(args, 2), pN = _a[0], data = _a[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[FIREBASE_LONGPOLL_START_PARAM] = 't';
|
|
urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = Math.floor(Math.random() * 100000000);
|
|
if (_this.scriptTagHolder.uniqueCallbackIdentifier) {
|
|
urlParams[FIREBASE_LONGPOLL_CALLBACK_ID_PARAM] =
|
|
_this.scriptTagHolder.uniqueCallbackIdentifier;
|
|
}
|
|
urlParams[VERSION_PARAM] = PROTOCOL_VERSION;
|
|
if (_this.transportSessionId) {
|
|
urlParams[TRANSPORT_SESSION_PARAM] = _this.transportSessionId;
|
|
}
|
|
if (_this.lastSessionId) {
|
|
urlParams[LAST_SESSION_PARAM] = _this.lastSessionId;
|
|
}
|
|
if (_this.applicationId) {
|
|
urlParams[APPLICATION_ID_PARAM] = _this.applicationId;
|
|
}
|
|
if (_this.appCheckToken) {
|
|
urlParams[APP_CHECK_TOKEN_PARAM] = _this.appCheckToken;
|
|
}
|
|
if (typeof location !== 'undefined' &&
|
|
location.hostname &&
|
|
FORGE_DOMAIN_RE.test(location.hostname)) {
|
|
urlParams[REFERER_PARAM] = 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 () {
|
|
if (util.isNodeSdk()) {
|
|
return false;
|
|
}
|
|
else if (BrowserPollConnection.forceAllow_) {
|
|
return true;
|
|
}
|
|
else {
|
|
// 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.forceDisallow_ &&
|
|
typeof document !== 'undefined' &&
|
|
document.createElement != null &&
|
|
!isChromeExtensionContentScript() &&
|
|
!isWindowsStoreApp());
|
|
}
|
|
};
|
|
/**
|
|
* No-op for polling
|
|
*/
|
|
BrowserPollConnection.prototype.markConnectionHealthy = function () { };
|
|
/**
|
|
* Stops polling and cleans up the iframe
|
|
*/
|
|
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
|
|
*/
|
|
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 data - The JSON data to transmit.
|
|
*/
|
|
BrowserPollConnection.prototype.send = function (data) {
|
|
var dataStr = util.stringify(data);
|
|
this.bytesSent += dataStr.length;
|
|
this.stats_.incrementCounter('bytes_sent', dataStr.length);
|
|
//first, lets get the base64-encoded data
|
|
var base64data = util.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 = 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).
|
|
*/
|
|
BrowserPollConnection.prototype.addDisconnectPingFrame = function (id, pw) {
|
|
if (util.isNodeSdk()) {
|
|
return;
|
|
}
|
|
this.myDisconnFrame = document.createElement('iframe');
|
|
var urlParams = {};
|
|
urlParams[FIREBASE_LONGPOLL_DISCONN_FRAME_REQUEST_PARAM] = 't';
|
|
urlParams[FIREBASE_LONGPOLL_ID_PARAM] = id;
|
|
urlParams[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
|
|
*/
|
|
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.stringify(args).length;
|
|
this.bytesReceived += bytesReceived;
|
|
this.stats_.incrementCounter('bytes_received', bytesReceived);
|
|
};
|
|
return BrowserPollConnection;
|
|
}());
|
|
/*********************************************************************************************
|
|
* A wrapper around an iframe that is used as a long-polling script holder.
|
|
*********************************************************************************************/
|
|
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.
|
|
this.outstandingRequests = new Set();
|
|
//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.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 = LUIDGenerator();
|
|
window[FIREBASE_LONGPOLL_COMMAND_CB_NAME + this.uniqueCallbackIdentifier] = commandCB;
|
|
window[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) {
|
|
log('frame writing exception');
|
|
if (e.stack) {
|
|
log(e.stack);
|
|
}
|
|
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.
|
|
*/
|
|
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.
|
|
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
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}
|
|
else if (iframe.document) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
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));
|
|
}
|
|
// 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 id - The ID of this connection
|
|
* @param 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.size < (this.pendingSegs.length > 0 ? 2 : 1)) {
|
|
//construct our url
|
|
this.currentSerial++;
|
|
var urlParams = {};
|
|
urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID;
|
|
urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW;
|
|
urlParams[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 +
|
|
'&' +
|
|
FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM +
|
|
i +
|
|
'=' +
|
|
theSeg.seg +
|
|
'&' +
|
|
FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET +
|
|
i +
|
|
'=' +
|
|
theSeg.ts +
|
|
'&' +
|
|
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 url - The URL of the script tag.
|
|
* @param serial - The serial number of the request.
|
|
*/
|
|
FirebaseIFrameScriptHolder.prototype.addLongPollTag_ = function (url, serial) {
|
|
var _this = this;
|
|
//remember that we sent this request.
|
|
this.outstandingRequests.add(serial);
|
|
var doNewRequest = function () {
|
|
_this.outstandingRequests.delete(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 url - The URL for the script tag source.
|
|
* @param loadCB - A callback to be triggered once the script has loaded.
|
|
*/
|
|
FirebaseIFrameScriptHolder.prototype.addTag = function (url, loadCB) {
|
|
var _this = this;
|
|
if (util.isNodeSdk()) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
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;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
newScript_1.onload = newScript_1.onreadystatechange =
|
|
function () {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
var rstate = newScript_1.readyState;
|
|
if (!rstate || rstate === 'loaded' || rstate === 'complete') {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
newScript_1.onload = newScript_1.onreadystatechange = null;
|
|
if (newScript_1.parentNode) {
|
|
newScript_1.parentNode.removeChild(newScript_1);
|
|
}
|
|
loadCB();
|
|
}
|
|
};
|
|
newScript_1.onerror = function () {
|
|
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;
|
|
}());
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
/**
|
|
* Currently simplistic, this class manages what transport a Connection should use at various stages of its
|
|
* lifecycle.
|
|
*
|
|
* It starts with longpolling in a browser, and httppolling on node. It then upgrades to websockets if
|
|
* they are available.
|
|
*/
|
|
var TransportManager = /** @class */ (function () {
|
|
/**
|
|
* @param repoInfo - Metadata around the namespace we're connecting to
|
|
*/
|
|
function TransportManager(repoInfo) {
|
|
this.initTransports_(repoInfo);
|
|
}
|
|
Object.defineProperty(TransportManager, "ALL_TRANSPORTS", {
|
|
get: function () {
|
|
return [BrowserPollConnection, WebSocketConnection];
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
TransportManager.prototype.initTransports_ = function (repoInfo) {
|
|
var e_1, _a;
|
|
var isWebSocketsAvailable = WebSocketConnection && WebSocketConnection['isAvailable']();
|
|
var isSkipPollConnection = isWebSocketsAvailable && !WebSocketConnection.previouslyFailed();
|
|
if (repoInfo.webSocketOnly) {
|
|
if (!isWebSocketsAvailable) {
|
|
warn("wss:// URL used, but browser isn't known to support websockets. Trying anyway.");
|
|
}
|
|
isSkipPollConnection = true;
|
|
}
|
|
if (isSkipPollConnection) {
|
|
this.transports_ = [WebSocketConnection];
|
|
}
|
|
else {
|
|
var transports = (this.transports_ = []);
|
|
try {
|
|
for (var _b = tslib.__values(TransportManager.ALL_TRANSPORTS), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
var transport = _c.value;
|
|
if (transport && transport['isAvailable']()) {
|
|
transports.push(transport);
|
|
}
|
|
}
|
|
}
|
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
finally {
|
|
try {
|
|
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
}
|
|
finally { if (e_1) throw e_1.error; }
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* @returns The constructor for the initial transport to use
|
|
*/
|
|
TransportManager.prototype.initialTransport = function () {
|
|
if (this.transports_.length > 0) {
|
|
return this.transports_[0];
|
|
}
|
|
else {
|
|
throw new Error('No transports available');
|
|
}
|
|
};
|
|
/**
|
|
* @returns The constructor for the next transport, or null
|
|
*/
|
|
TransportManager.prototype.upgradeTransport = function () {
|
|
if (this.transports_.length > 1) {
|
|
return this.transports_[1];
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
};
|
|
return TransportManager;
|
|
}());
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* 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.
|
|
*/
|
|
// 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.
|
|
*/
|
|
var Connection = /** @class */ (function () {
|
|
/**
|
|
* @param id - an id for this connection
|
|
* @param repoInfo_ - the info for the endpoint to connect to
|
|
* @param applicationId_ - the Firebase App ID for this project
|
|
* @param appCheckToken_ - The App Check Token for this device.
|
|
* @param authToken_ - The auth token for this session.
|
|
* @param onMessage_ - the callback to be triggered when a server-push message arrives
|
|
* @param onReady_ - the callback to be triggered when this connection is ready to send messages.
|
|
* @param onDisconnect_ - the callback to be triggered when a connection was lost
|
|
* @param onKill_ - the callback to be triggered when this connection has permanently shut down.
|
|
* @param lastSessionId - last session id in persistent connection. is used to clean up old session in real-time server
|
|
*/
|
|
function Connection(id, repoInfo_, applicationId_, appCheckToken_, authToken_, onMessage_, onReady_, onDisconnect_, onKill_, lastSessionId) {
|
|
this.id = id;
|
|
this.repoInfo_ = repoInfo_;
|
|
this.applicationId_ = applicationId_;
|
|
this.appCheckToken_ = appCheckToken_;
|
|
this.authToken_ = authToken_;
|
|
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_ = logWrapper('c:' + this.id + ':');
|
|
this.transportManager_ = new TransportManager(repoInfo_);
|
|
this.log_('Connection created');
|
|
this.start_();
|
|
}
|
|
/**
|
|
* Starts a connection attempt
|
|
*/
|
|
Connection.prototype.start_ = function () {
|
|
var _this = this;
|
|
var conn = this.transportManager_.initialTransport();
|
|
this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, this.appCheckToken_, this.authToken_, null, 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 healthyTimeoutMS = conn['healthyTimeout'] || 0;
|
|
if (healthyTimeoutMS > 0) {
|
|
this.healthyTimeout_ = 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();
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}, Math.floor(healthyTimeoutMS));
|
|
}
|
|
};
|
|
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_();
|
|
}
|