Files
2021-12-29 01:57:42 -08:00

13969 lines
532 KiB
JavaScript

import { getApp, _getProvider, SDK_VERSION as SDK_VERSION$1, _registerComponent, registerVersion } from '@firebase/app';
import { Component } from '@firebase/component';
import { stringify, jsonEval, contains, assert, base64, stringToByteArray, Sha1, isNodeSdk, deepCopy, base64Encode, isMobileCordova, stringLength, Deferred, safeGet, isAdmin, isValidFormat, isEmpty, isReactNative, assertionError, map, querystring, errorPrefix, getModularInstance, createMockUserToken } from '@firebase/util';
import { Logger, LogLevel } from '@firebase/logger';
const name = "@firebase/database";
const version = "0.12.4";
/**
* @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. */
let 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.
*/
/**
* 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.
*
*/
class DOMStorageWrapper {
/**
* @param domStorage_ - The underlying storage object (e.g. localStorage or sessionStorage)
*/
constructor(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.
*/
set(key, value) {
if (value == null) {
this.domStorage_.removeItem(this.prefixedName_(key));
}
else {
this.domStorage_.setItem(this.prefixedName_(key), stringify(value));
}
}
/**
* @returns The value that was stored under this key, or null
*/
get(key) {
const storedVal = this.domStorage_.getItem(this.prefixedName_(key));
if (storedVal == null) {
return null;
}
else {
return jsonEval(storedVal);
}
}
remove(key) {
this.domStorage_.removeItem(this.prefixedName_(key));
}
prefixedName_(name) {
return this.prefix_ + name;
}
toString() {
return this.domStorage_.toString();
}
}
/**
* @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).
*/
class MemoryStorage {
constructor() {
this.cache_ = {};
this.isInMemoryStorage = true;
}
set(key, value) {
if (value == null) {
delete this.cache_[key];
}
else {
this.cache_[key] = value;
}
}
get(key) {
if (contains(this.cache_, key)) {
return this.cache_[key];
}
return null;
}
remove(key) {
delete this.cache_[key];
}
}
/**
* @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.
*/
const 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
const 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 */
const PersistentStorage = createStoragefor('localStorage');
/** A storage object that only lasts one session */
const 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.
*/
const logClient = new Logger('@firebase/database');
/**
* Returns a locally-unique ID (generated by just incrementing up from 0 each time its called).
*/
const LUIDGenerator = (function () {
let id = 1;
return function () {
return id++;
};
})();
/**
* Sha1 hash of the input string
* @param str - The string to hash
* @returns {!string} The resulting hash
*/
const sha1 = function (str) {
const utf8Bytes = stringToByteArray(str);
const sha1 = new Sha1();
sha1.update(utf8Bytes);
const sha1Bytes = sha1.digest();
return base64.encodeByteArray(sha1Bytes);
};
const buildLogMessage_ = function (...varArgs) {
let message = '';
for (let i = 0; i < varArgs.length; i++) {
const 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 += stringify(arg);
}
else {
message += arg;
}
message += ' ';
}
return message;
};
/**
* Use this for all debug messages in Firebase.
*/
let logger = null;
/**
* Flag to check for log availability on first log message
*/
let 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
*/
const enableLogging$1 = function (logger_, persistent) {
assert(!persistent || logger_ === true || logger_ === false, "Can't turn on custom loggers persistently.");
if (logger_ === true) {
logClient.logLevel = 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');
}
};
const log = function (...varArgs) {
if (firstLog_ === true) {
firstLog_ = false;
if (logger === null && SessionStorage.get('logging_enabled') === true) {
enableLogging$1(true);
}
}
if (logger) {
const message = buildLogMessage_.apply(null, varArgs);
logger(message);
}
};
const logWrapper = function (prefix) {
return function (...varArgs) {
log(prefix, ...varArgs);
};
};
const error = function (...varArgs) {
const message = 'FIREBASE INTERNAL ERROR: ' + buildLogMessage_(...varArgs);
logClient.error(message);
};
const fatal = function (...varArgs) {
const message = `FIREBASE FATAL ERROR: ${buildLogMessage_(...varArgs)}`;
logClient.error(message);
throw new Error(message);
};
const warn = function (...varArgs) {
const message = 'FIREBASE WARNING: ' + buildLogMessage_(...varArgs);
logClient.warn(message);
};
/**
* Logs a warning if the containing page uses https. Called when a call to new Firebase
* does not use https.
*/
const 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.
*/
const isInvalidJSONNumber = function (data) {
return (typeof data === 'number' &&
(data !== data || // NaN
data === Number.POSITIVE_INFINITY ||
data === Number.NEGATIVE_INFINITY));
};
const executeWhenDOMReady = function (fn) {
if (isNodeSdk() || document.readyState === 'complete') {
fn();
}
else {
// Modeled after jQuery. Try DOMContentLoaded and onreadystatechange (which
// fire before onload), but fall back to onload.
let called = false;
const wrappedFn = function () {
if (!document.body) {
setTimeout(wrappedFn, Math.floor(10));
return;
}
if (!called) {
called = true;
fn();
}
};
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', wrappedFn, false);
// fallback to onload.
window.addEventListener('load', wrappedFn, 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', () => {
if (document.readyState === 'complete') {
wrappedFn();
}
});
// fallback to onload.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.attachEvent('onload', wrappedFn);
// 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
*/
const MIN_NAME = '[MIN_NAME]';
/**
* Maximum key name. Invalid for actual data, used as a marker to sort above any valid names
*/
const MAX_NAME = '[MAX_NAME]';
/**
* Compares valid Firebase key names, plus min and max name
*/
const 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 {
const 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.
*/
const stringCompare = function (a, b) {
if (a === b) {
return 0;
}
else if (a < b) {
return -1;
}
else {
return 1;
}
};
const requireKey = function (key, obj) {
if (obj && key in obj) {
return obj[key];
}
else {
throw new Error('Missing required key (' + key + ') in object: ' + stringify(obj));
}
};
const ObjectToUniqueKey = function (obj) {
if (typeof obj !== 'object' || obj === null) {
return stringify(obj);
}
const keys = [];
// eslint-disable-next-line guard-for-in
for (const k in obj) {
keys.push(k);
}
// Export as json, but with the keys sorted.
keys.sort();
let key = '{';
for (let i = 0; i < keys.length; i++) {
if (i !== 0) {
key += ',';
}
key += 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
*/
const splitStringBySize = function (str, segsize) {
const len = str.length;
if (len <= segsize) {
return [str];
}
const dataSegs = [];
for (let 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 (const 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
*
*/
const doubleToIEEE754String = function (v) {
assert(!isInvalidJSONNumber(v), 'Invalid JSON number'); // MJL
const ebits = 11, fbits = 52;
const bias = (1 << (ebits - 1)) - 1;
let 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
const 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();
const str = bits.join('');
// Return the data as a hex string. --MJL
let hexByteString = '';
for (i = 0; i < 64; i += 8) {
let 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).
*/
const 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.
*/
const 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) {
let 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';
}
const 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
*/
const INTEGER_REGEXP_ = new RegExp('^-?(0*)\\d{1,10}$');
/**
* For use in keys, the minimum possible 32-bit integer.
*/
const INTEGER_32_MIN = -2147483648;
/**
* For use in kyes, the maximum possible 32-bit integer.
*/
const INTEGER_32_MAX = 2147483647;
/**
* If the string contains a 32-bit integer, return it. Else return null.
*/
const tryParseInt = function (str) {
if (INTEGER_REGEXP_.test(str)) {
const 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.
*/
const exceptionGuard = function (fn) {
try {
fn();
}
catch (e) {
// Re-throw exception when it's safe.
setTimeout(() => {
// 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.
const 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.
*/
const beingCrawled = function () {
const 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.
*/
const setTimeoutNonBlocking = function (fn, time) {
const 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 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.
*/
class AppCheckTokenProvider {
constructor(appName_, appCheckProvider) {
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(appCheck => (this.appCheck = appCheck));
}
}
getToken(forceRefresh) {
if (!this.appCheck) {
return new Promise((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(() => {
if (this.appCheck) {
this.getToken(forceRefresh).then(resolve, reject);
}
else {
resolve(null);
}
}, 0);
});
}
return this.appCheck.getToken(forceRefresh);
}
addTokenChangeListener(listener) {
var _a;
(_a = this.appCheckProvider) === null || _a === void 0 ? void 0 : _a.get().then(appCheck => appCheck.addTokenListener(listener));
}
notifyForInvalidToken() {
warn(`Provided AppCheck credentials for the app named "${this.appName_}" ` +
'are invalid. This usually indicates your app was not initialized correctly.');
}
}
/**
* @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.
*/
class FirebaseAuthTokenProvider {
constructor(appName_, firebaseOptions_, authProvider_) {
this.appName_ = appName_;
this.firebaseOptions_ = firebaseOptions_;
this.authProvider_ = authProvider_;
this.auth_ = null;
this.auth_ = authProvider_.getImmediate({ optional: true });
if (!this.auth_) {
authProvider_.onInit(auth => (this.auth_ = auth));
}
}
getToken(forceRefresh) {
if (!this.auth_) {
return new Promise((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(() => {
if (this.auth_) {
this.getToken(forceRefresh).then(resolve, reject);
}
else {
resolve(null);
}
}, 0);
});
}
return this.auth_.getToken(forceRefresh).catch(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);
}
});
}
addTokenChangeListener(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(auth => auth.addAuthTokenListener(listener));
}
}
removeTokenChangeListener(listener) {
this.authProvider_
.get()
.then(auth => auth.removeAuthTokenListener(listener));
}
notifyForInvalidToken() {
let 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);
}
}
/* AuthTokenProvider that supplies a constant token. Used by Admin SDK or mockUserToken with emulators. */
class EmulatorTokenProvider {
constructor(accessToken) {
this.accessToken = accessToken;
}
getToken(forceRefresh) {
return Promise.resolve({
accessToken: this.accessToken
});
}
addTokenChangeListener(listener) {
// Invoke the listener immediately to match the behavior in Firebase Auth
// (see packages/auth/src/auth.js#L1807)
listener(this.accessToken);
}
removeTokenChangeListener(listener) { }
notifyForInvalidToken() { }
}
/** A string that is treated as an admin access token by the RTDB emulator. Used by Admin SDK. */
EmulatorTokenProvider.OWNER = 'owner';
/**
* @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.
*/
const PROTOCOL_VERSION = '5';
const VERSION_PARAM = 'v';
const TRANSPORT_SESSION_PARAM = 's';
const REFERER_PARAM = 'r';
const FORGE_REF = 'f';
// Matches console.firebase.google.com, firebase-console-*.corp.google.com and
// firebase.corp.google.com
const FORGE_DOMAIN_RE = /(console\.firebase|firebase-console-\w+\.corp|firebase\.corp)\.google\.com/;
const LAST_SESSION_PARAM = 'ls';
const APPLICATION_ID_PARAM = 'p';
const APP_CHECK_TOKEN_PARAM = 'ac';
const WEBSOCKET = 'websocket';
const 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.
*/
/**
* A class that holds metadata about a Repo object
*/
class RepoInfo {
/**
* @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
*/
constructor(host, secure, namespace, webSocketOnly, nodeAdmin = false, persistenceKey = '', 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;
}
isCacheableHost() {
return this.internalHost.substr(0, 2) === 's-';
}
isCustomHost() {
return (this._domain !== 'firebaseio.com' &&
this._domain !== 'firebaseio-demo.com');
}
get host() {
return this._host;
}
set host(newHost) {
if (newHost !== this.internalHost) {
this.internalHost = newHost;
if (this.isCacheableHost()) {
PersistentStorage.set('host:' + this._host, this.internalHost);
}
}
}
toString() {
let str = this.toURLString();
if (this.persistenceKey) {
str += '<' + this.persistenceKey + '>';
}
return str;
}
toURLString() {
const protocol = this.secure ? 'https://' : 'http://';
const query = this.includeNamespaceInQueryParams
? `?ns=${this.namespace}`
: '';
return `${protocol}${this.host}/${query}`;
}
}
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) {
assert(typeof type === 'string', 'typeof type must == string');
assert(typeof params === 'object', 'typeof params must == object');
let 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;
}
const pairs = [];
each(params, (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.
*/
class StatsCollection {
constructor() {
this.counters_ = {};
}
incrementCounter(name, amount = 1) {
if (!contains(this.counters_, name)) {
this.counters_[name] = 0;
}
this.counters_[name] += amount;
}
get() {
return deepCopy(this.counters_);
}
}
/**
* @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.
*/
const collections = {};
const reporters = {};
function statsManagerGetCollection(repoInfo) {
const hashString = repoInfo.toString();
if (!collections[hashString]) {
collections[hashString] = new StatsCollection();
}
return collections[hashString];
}
function statsManagerGetOrCreateReporter(repoInfo, creatorFunction) {
const hashString = repoInfo.toString();
if (!reporters[hashString]) {
reporters[hashString] = creatorFunction();
}
return reporters[hashString];
}
/**
* @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.
*/
class PacketReceiver {
/**
* @param onMessage_
*/
constructor(onMessage_) {
this.onMessage_ = onMessage_;
this.pendingResponses = [];
this.currentResponseNum = 0;
this.closeAfterResponse = -1;
this.onClose = null;
}
closeAfter(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
*/
handleResponse(requestNum, data) {
this.pendingResponses[requestNum] = data;
while (this.pendingResponses[this.currentResponseNum]) {
const toProcess = this.pendingResponses[this.currentResponseNum];
delete this.pendingResponses[this.currentResponseNum];
for (let i = 0; i < toProcess.length; ++i) {
if (toProcess[i]) {
exceptionGuard(() => {
this.onMessage_(toProcess[i]);
});
}
}
if (this.currentResponseNum === this.closeAfterResponse) {
if (this.onClose) {
this.onClose();
this.onClose = null;
}
break;
}
this.currentResponseNum++;
}
}
}
/**
* @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
const FIREBASE_LONGPOLL_START_PARAM = 'start';
const FIREBASE_LONGPOLL_CLOSE_COMMAND = 'close';
const FIREBASE_LONGPOLL_COMMAND_CB_NAME = 'pLPCommand';
const FIREBASE_LONGPOLL_DATA_CB_NAME = 'pRTLPCB';
const FIREBASE_LONGPOLL_ID_PARAM = 'id';
const FIREBASE_LONGPOLL_PW_PARAM = 'pw';
const FIREBASE_LONGPOLL_SERIAL_PARAM = 'ser';
const FIREBASE_LONGPOLL_CALLBACK_ID_PARAM = 'cb';
const FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM = 'seg';
const FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET = 'ts';
const FIREBASE_LONGPOLL_DATA_PARAM = 'd';
const 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.
const MAX_URL_DATA_SIZE = 1870;
const SEG_HEADER_SIZE = 30; //ie: &seg=8299234&ts=982389123&d=
const 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 KEEPALIVE_REQUEST_INTERVAL = 25000;
/**
* How long to wait before aborting a long-polling connection attempt.
*/
const LP_CONNECT_TIMEOUT = 30000;
/**
* This class manages a single long-polling connection.
*/
class BrowserPollConnection {
/**
* @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
*/
constructor(connId, repoInfo, applicationId, appCheckToken, authToken, transportSessionId, lastSessionId) {
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 = (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.
*/
open(onMessage, onDisconnect) {
this.curSegmentNum = 0;
this.onDisconnect_ = onDisconnect;
this.myPacketOrderer = new PacketReceiver(onMessage);
this.isClosed_ = false;
this.connectTimeoutTimer_ = setTimeout(() => {
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(() => {
if (this.isClosed_) {
return;
}
//Set up a callback that gets triggered once a connection is set up.
this.scriptTagHolder = new FirebaseIFrameScriptHolder((...args) => {
const [command, arg1, arg2, arg3, arg4] = args;
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, () => {
this.onClosed_();
});
}
else {
this.onClosed_();
}
}
else {
throw new Error('Unrecognized command received: ' + command);
}
}, (...args) => {
const [pN, data] = args;
this.incrementIncomingBytes_(args);
this.myPacketOrderer.handleResponse(pN, data);
}, () => {
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.
const 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;
}
const connectURL = this.urlFn(urlParams);
this.log_('Connecting via long-poll to ' + connectURL);
this.scriptTagHolder.addTag(connectURL, () => {
/* do nothing */
});
});
}
/**
* Call this when a handshake has completed successfully and we want to consider the connection established
*/
start() {
this.scriptTagHolder.startLongPoll(this.id, this.password);
this.addDisconnectPingFrame(this.id, this.password);
}
/**
* Forces long polling to be considered as a potential transport
*/
static forceAllow() {
BrowserPollConnection.forceAllow_ = true;
}
/**
* Forces longpolling to not be considered as a potential transport
*/
static forceDisallow() {
BrowserPollConnection.forceDisallow_ = true;
}
// Static method, use string literal so it can be accessed in a generic way
static isAvailable() {
if (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
*/
markConnectionHealthy() { }
/**
* Stops polling and cleans up the iframe
*/
shutdown_() {
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
*/
onClosed_() {
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.
*/
close() {
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.
*/
send(data) {
const dataStr = stringify(data);
this.bytesSent += dataStr.length;
this.stats_.incrementCounter('bytes_sent', dataStr.length);
//first, lets get the base64-encoded data
const base64data = 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.
const 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 (let 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).
*/
addDisconnectPingFrame(id, pw) {
if (isNodeSdk()) {
return;
}
this.myDisconnFrame = document.createElement('iframe');
const 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
*/
incrementIncomingBytes_(args) {
// TODO: This is an annoying perf hit just to track the number of incoming bytes. Maybe it should be opt-in.
const bytesReceived = stringify(args).length;
this.bytesReceived += bytesReceived;
this.stats_.incrementCounter('bytes_received', bytesReceived);
}
}
/*********************************************************************************************
* A wrapper around an iframe that is used as a long-polling script holder.
*********************************************************************************************/
class FirebaseIFrameScriptHolder {
/**
* @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.
*/
constructor(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 (!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.
let 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:') {
const currentDomain = document.domain;
script = '<script>document.domain="' + currentDomain + '";</script>';
}
const 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.
*/
static createIFrame_() {
const 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.
const 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) {
const 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.
*/
close() {
//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(() => {
if (this.myIFrame !== null) {
document.body.removeChild(this.myIFrame);
this.myIFrame = null;
}
}, Math.floor(0));
}
// Protect from being called recursively.
const 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
*/
startLongPoll(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.
*/
newRequest_() {
// 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++;
const urlParams = {};
urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID;
urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW;
urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = this.currentSerial;
let theURL = this.urlFn(urlParams);
//Now add as much data as we can.
let curDataString = '';
let i = 0;
while (this.pendingSegs.length > 0) {
//first, lets see if the next segment will fit.
const 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.
const 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.
*/
enqueueSegment(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.
*/
addLongPollTag_(url, serial) {
//remember that we sent this request.
this.outstandingRequests.add(serial);
const doNewRequest = () => {
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.
const keepaliveTimeout = setTimeout(doNewRequest, Math.floor(KEEPALIVE_REQUEST_INTERVAL));
const readyStateCB = () => {
// 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.
*/
addTag(url, loadCB) {
if (isNodeSdk()) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.doNodeLongPoll(url, loadCB);
}
else {
setTimeout(() => {
try {
// if we're already closed, don't add this poll
if (!this.sendNewPolls) {
return;
}
const newScript = this.myIFrame.doc.createElement('script');
newScript.type = 'text/javascript';
newScript.async = true;
newScript.src = url;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newScript.onload = newScript.onreadystatechange =
function () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rstate = newScript.readyState;
if (!rstate || rstate === 'loaded' || rstate === 'complete') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newScript.onload = newScript.onreadystatechange = null;
if (newScript.parentNode) {
newScript.parentNode.removeChild(newScript);
}
loadCB();
}
};
newScript.onerror = () => {
log('Long-poll script failed to load: ' + url);
this.sendNewPolls = false;
this.close();
};
this.myIFrame.doc.body.appendChild(newScript);
}
catch (e) {
// TODO: we should make this error visible somehow
}
}, Math.floor(1));
}
}
}
/**
* @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.
*/
const WEBSOCKET_MAX_FRAME_SIZE = 16384;
const WEBSOCKET_KEEPALIVE_INTERVAL = 45000;
let WebSocketImpl = null;
if (typeof MozWebSocket !== 'undefined') {
WebSocketImpl = MozWebSocket;
}
else if (typeof WebSocket !== 'undefined') {
WebSocketImpl = WebSocket;
}
/**
* Create a new websocket connection with the given callbacks.
*/
class WebSocketConnection {
/**
* @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
*/
constructor(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
*/
static connectionURL_(repoInfo, transportSessionId, lastSessionId, appCheckToken) {
const urlParams = {};
urlParams[VERSION_PARAM] = PROTOCOL_VERSION;
if (!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.
*/
open(onMessage, onDisconnect) {
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 (isNodeSdk()) {
const device = this.nodeAdmin ? 'AdminNode' : 'Node';
// UA Format: Firebase/<wire_protocol>/<sdk_version>/<platform>/<device>
const 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.
const env = process['env'];
const 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 {
const 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.');
const error = e.message || e.data;
if (error) {
this.log_(error);
}
this.onClosed_();
return;
}
this.mySock.onopen = () => {
this.log_('Websocket connected.');
this.everConnected_ = true;
};
this.mySock.onclose = () => {
this.log_('Websocket connection was disconnected.');
this.mySock = null;
this.onClosed_();
};
this.mySock.onmessage = m => {
this.handleIncomingFrame(m);
};
this.mySock.onerror = e => {
this.log_('WebSocket error. Closing connection.');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const 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
*/
start() { }
static forceDisallow() {
WebSocketConnection.forceDisallow_ = true;
}
static isAvailable() {
let isOldAndroid = false;
if (typeof navigator !== 'undefined' && navigator.userAgent) {
const oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/;
const 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.
*/
static previouslyFailed() {
// 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);
}
markConnectionHealthy() {
PersistentStorage.remove('previous_websocket_failure');
}
appendFrame_(data) {
this.frames.push(data);
if (this.frames.length === this.totalFrames) {
const fullMess = this.frames.join('');
this.frames = null;
const jsonMess = jsonEval(fullMess);
//handle the message
this.onMessage(jsonMess);
}
}
/**
* @param frameCount - The number of frames we are expecting from the server
*/
handleNewFrameCount_(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
*/
extractFrameCount_(data) {
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) {
const 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
*/
handleIncomingFrame(mess) {
if (this.mySock === null) {
return; // Chrome apparently delivers incoming packets even after we .close() the connection sometimes.
}
const 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
const remainingData = this.extractFrameCount_(data);
if (remainingData !== null) {
this.appendFrame_(remainingData);
}
}
}
/**
* Send a message to the server
* @param data - The JSON object to transmit
*/
send(data) {
this.resetKeepAlive();
const dataStr = 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.
const 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 (let i = 0; i < dataSegs.length; i++) {
this.sendString_(dataSegs[i]);
}
}
shutdown_() {
this.isClosed_ = true;
if (this.keepaliveTimer) {
clearInterval(this.keepaliveTimer);
this.keepaliveTimer = null;
}
if (this.mySock) {
this.mySock.close();
this.mySock = null;
}
}
onClosed_() {
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.
*/
close() {
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.
*/
resetKeepAlive() {
clearInterval(this.keepaliveTimer);
this.keepaliveTimer = setInterval(() => {
//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.
*/
sendString_(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;
/**
* @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.
*/
class TransportManager {
/**
* @param repoInfo - Metadata around the namespace we're connecting to
*/
constructor(repoInfo) {
this.initTransports_(repoInfo);
}
static get ALL_TRANSPORTS() {
return [BrowserPollConnection, WebSocketConnection];
}
initTransports_(repoInfo) {
const isWebSocketsAvailable = WebSocketConnection && WebSocketConnection['isAvailable']();
let 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 {
const transports = (this.transports_ = []);
for (const transport of TransportManager.ALL_TRANSPORTS) {
if (transport && transport['isAvailable']()) {
transports.push(transport);
}
}
}
}
/**
* @returns The constructor for the initial transport to use
*/
initialTransport() {
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
*/
upgradeTransport() {
if (this.transports_.length > 1) {
return this.transports_[1];
}
else {
return null;
}
}
}
/**
* @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.
const 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.
const 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.
const BYTES_SENT_HEALTHY_OVERRIDE = 10 * 1024;
const BYTES_RECEIVED_HEALTHY_OVERRIDE = 100 * 1024;
const MESSAGE_TYPE = 't';
const MESSAGE_DATA = 'd';
const CONTROL_SHUTDOWN = 's';
const CONTROL_RESET = 'r';
const CONTROL_ERROR = 'e';
const CONTROL_PONG = 'o';
const SWITCH_ACK = 'a';
const END_TRANSMISSION = 'n';
const PING = 'p';
const SERVER_HELLO = 'h';
/**
* Creates a new real-time connection to the server using whichever method works
* best in the current browser.
*/
class Connection {
/**
* @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
*/
constructor(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
*/
start_() {
const 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;
const onMessageReceived = this.connReceiver_(this.conn_);
const 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(() => {
// 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));
const healthyTimeoutMS = conn['healthyTimeout'] || 0;
if (healthyTimeoutMS > 0) {
this.healthyTimeout_ = setTimeoutNonBlocking(() => {
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));
}
}
nextTransportId_() {
return 'c:' + this.id + ':' + this.connectionCount++;
}
disconnReceiver_(conn) {
return everConnected => {
if (conn === this.conn_) {
this.onConnectionLost_(everConnected);
}
else if (conn === this.secondaryConn_) {
this.log_('Secondary connection lost.');
this.onSecondaryConnectionLost_();
}
else {
this.log_('closing an old connection');
}
};
}
connReceiver_(conn) {
return (message) => {
if (this.state_ !== 2 /* DISCONNECTED */) {
if (conn === this.rx_) {
this.onPrimaryMessageReceived_(message);
}
else if (conn === this.secondaryConn_) {
this.onSecondaryMessageReceived_(message);
}
else {
this.log_('message on old connection');
}
}
};
}
/**
* @param dataMsg - An arbitrary data message to be sent to the server
*/
sendRequest(dataMsg) {
// wrap in a data message envelope and send it on
const msg = { t: 'd', d: dataMsg };
this.sendData_(msg);
}
tryCleanupConnection() {
if (this.tx_ === this.secondaryConn_ && this.rx_ === this.secondaryConn_) {
this.log_('cleaning up and promoting a connection: ' + this.secondaryConn_.connId);
this.conn_ = this.secondaryConn_;
this.secondaryConn_ = null;
// the server will shutdown the old connection
}
}
onSecondaryControl_(controlData) {
if (MESSAGE_TYPE in controlData) {
const cmd = controlData[MESSAGE_TYPE];
if (cmd === SWITCH_ACK) {
this.upgradeIfSecondaryHealthy_();
}
else if (cmd === CONTROL_RESET) {
// Most likely the session wasn't valid. Abandon the switch attempt
this.log_('Got a reset on secondary, closing it');
this.secondaryConn_.close();
// If we were already using this connection for something, than we need to fully close
if (this.tx_ === this.secondaryConn_ ||
this.rx_ === this.secondaryConn_) {
this.close();
}
}
else if (cmd === CONTROL_PONG) {
this.log_('got pong on secondary.');
this.secondaryResponsesRequired_--;
this.upgradeIfSecondaryHealthy_();
}
}
}
onSecondaryMessageReceived_(parsedData) {
const layer = requireKey('t', parsedData);
const data = requireKey('d', parsedData);
if (layer === 'c') {
this.onSecondaryControl_(data);
}
else if (layer === 'd') {
// got a data message, but we're still second connection. Need to buffer it up
this.pendingDataMessages.push(data);
}
else {
throw new Error('Unknown protocol layer: ' + layer);
}
}
upgradeIfSecondaryHealthy_() {
if (this.secondaryResponsesRequired_ <= 0) {
this.log_('Secondary connection is healthy.');
this.isHealthy_ = true;
this.secondaryConn_.markConnectionHealthy();
this.proceedWithUpgrade_();
}
else {
// Send a ping to make sure the connection is healthy.
this.log_('sending ping on secondary.');
this.secondaryConn_.send({ t: 'c', d: { t: PING, d: {} } });
}
}
proceedWithUpgrade_() {
// tell this connection to consider itself open
this.secondaryConn_.start();
// send ack
this.log_('sending client ack on secondary');
this.secondaryConn_.send({ t: 'c', d: { t: SWITCH_ACK, d: {} } });
// send end packet on primary transport, switch to sending on this one
// can receive on this one, buffer responses until end received on primary transport
this.log_('Ending transmission on primary');
this.conn_.send({ t: 'c', d: { t: END_TRANSMISSION, d: {} } });
this.tx_ = this.secondaryConn_;
this.tryCleanupConnection();
}
onPrimaryMessageReceived_(parsedData) {
// Must refer to parsedData properties in quotes, so closure doesn't touch them.
const layer = requireKey('t', parsedData);
const data = requireKey('d', parsedData);
if (layer === 'c') {
this.onControl_(data);
}
else if (layer === 'd') {
this.onDataMessage_(data);
}
}
onDataMessage_(message) {
this.onPrimaryResponse_();
// We don't do anything with data messages, just kick them up a level
this.onMessage_(message);
}
onPrimaryResponse_() {
if (!this.isHealthy_) {
this.