@firebase/database
Version:
This is the Firebase Realtime Database component of the Firebase JS SDK.
1,415 lines (1,398 loc) • 554 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var Websocket = require('faye-websocket');
var util = require('@firebase/util');
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.
*/
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.
*/
/**
* 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), util.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 util.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 (util.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$1.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 = util.stringToByteArray(str);
const sha1 = new util.Sha1();
sha1.update(utf8Bytes);
const sha1Bytes = sha1.digest();
return util.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 += util.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) {
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');
}
};
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 (util.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: ' + util.stringify(obj));
}
};
const ObjectToUniqueKey = function (obj) {
if (typeof obj !== 'object' || obj === null) {
return util.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 += 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
*/
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) {
util.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 keys, 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);
// Note: at the time of this comment, unrefTimer is under the unstable set of APIs. Run with --unstable to enable the API.
if (typeof timeout === 'number' &&
// @ts-ignore Is only defined in Deno environments.
typeof Deno !== 'undefined' &&
// @ts-ignore Deno and unrefTimer are only defined in Deno environments.
Deno['unrefTimer']) {
// @ts-ignore Deno and unrefTimer are only defined in Deno environments.
Deno.unrefTimer(timeout);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
else 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
*/
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, isUsingEmulator = false, emulatorOptions = null) {
this.secure = secure;
this.namespace = namespace;
this.webSocketOnly = webSocketOnly;
this.nodeAdmin = nodeAdmin;
this.persistenceKey = persistenceKey;
this.includeNamespaceInQueryParams = includeNamespaceInQueryParams;
this.isUsingEmulator = isUsingEmulator;
this.emulatorOptions = emulatorOptions;
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) {
util.assert(typeof type === 'string', 'typeof type must == string');
util.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 (!util.contains(this.counters_, name)) {
this.counters_[name] = 0;
}
this.counters_[name] += amount;
}
get() {
return util.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 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.
*/
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;
}
function setWebSocketImpl(impl) {
WebSocketImpl = impl;
}
/**
* 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, applicationId);
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, applicationId) {
const 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;
}
if (applicationId) {
urlParams[APPLICATION_ID_PARAM] = applicationId;
}
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 {
let options;
if (util.isNodeSdk()) {
const device = this.nodeAdmin ? 'AdminNode' : 'Node';
// UA Format: Firebase/<wire_protocol>/<sdk_version>/<platform>/<device>
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);
}
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 = util.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) {
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) {
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 = 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.
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;
const name = "@firebase/database";
const version = "1.1.0";
/**
* @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(app$1, appCheckProvider) {
this.appCheckProvider = appCheckProvider;
this.appName = app$1.name;
if (app._isFirebaseServerApp(app$1) && app$1.settings.appCheckToken) {
this.serverAppAppCheckToken = app$1.settings.appCheckToken;
}
this.appCheck = appCheckProvider?.getImmediate({ optional: true });
if (!this.appCheck) {
appCheckProvider?.get().then(appCheck => (this.appCheck = appCheck));
}
}
getToken(forceRefresh) {
if (this.serverAppAppCheckToken) {
if (forceRefresh) {
throw new Error('Attempted reuse of `FirebaseServerApp.appCheckToken` after previous usage failed.');
}
return Promise.resolve({ token: this.serverAppAppCheckToken });
}
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 timeout below expires.
setTimeout(() => {
if (this.appCheck) {
this.getToken(forceRefresh).then(resolve, reject);
}
else {
resolve(null);
}
}, 0);
});
}
return this.appCheck.getToken(forceRefresh);
}
addTokenChangeListener(listener) {
this.appCheckProvider
?.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 timeout 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