(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@firebase/app')) : typeof define === 'function' && define.amd ? define(['exports', '@firebase/app'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.firebase = global.firebase || {}, global.firebase.analytics = global.firebase.analytics || {}), global.firebase.app)); }(this, (function (exports, app) { 'use strict'; try { (function() { /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } function __values(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); } function __read(o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; } function __spread() { for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); return ar; } /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __spreadArrays() { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; } /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var _a; /** * The JS SDK supports 5 log levels and also allows a user the ability to * silence the logs altogether. * * The order is a follows: * DEBUG < VERBOSE < INFO < WARN < ERROR * * All of the log types above the current log level will be captured (i.e. if * you set the log level to `INFO`, errors will still be logged, but `DEBUG` and * `VERBOSE` logs will not) */ var LogLevel; (function (LogLevel) { LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG"; LogLevel[LogLevel["VERBOSE"] = 1] = "VERBOSE"; LogLevel[LogLevel["INFO"] = 2] = "INFO"; LogLevel[LogLevel["WARN"] = 3] = "WARN"; LogLevel[LogLevel["ERROR"] = 4] = "ERROR"; LogLevel[LogLevel["SILENT"] = 5] = "SILENT"; })(LogLevel || (LogLevel = {})); var levelStringToEnum = { 'debug': LogLevel.DEBUG, 'verbose': LogLevel.VERBOSE, 'info': LogLevel.INFO, 'warn': LogLevel.WARN, 'error': LogLevel.ERROR, 'silent': LogLevel.SILENT }; /** * The default log level */ var defaultLogLevel = LogLevel.INFO; /** * By default, `console.debug` is not displayed in the developer console (in * chrome). To avoid forcing users to have to opt-in to these logs twice * (i.e. once for firebase, and once in the console), we are sending `DEBUG` * logs to the `console.log` function. */ var ConsoleMethod = (_a = {}, _a[LogLevel.DEBUG] = 'log', _a[LogLevel.VERBOSE] = 'log', _a[LogLevel.INFO] = 'info', _a[LogLevel.WARN] = 'warn', _a[LogLevel.ERROR] = 'error', _a); /** * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR * messages on to their corresponding console counterparts (if the log method * is supported by the current log level) */ var defaultLogHandler = function (instance, logType) { var args = []; for (var _i = 2; _i < arguments.length; _i++) { args[_i - 2] = arguments[_i]; } if (logType < instance.logLevel) { return; } var now = new Date().toISOString(); var method = ConsoleMethod[logType]; if (method) { console[method].apply(console, __spreadArrays(["[" + now + "] " + instance.name + ":"], args)); } else { throw new Error("Attempted to log a message with an invalid logType (value: " + logType + ")"); } }; var Logger = /** @class */ (function () { /** * Gives you an instance of a Logger to capture messages according to * Firebase's logging scheme. * * @param name The name that the logs will be associated with */ function Logger(name) { this.name = name; /** * The log level of the given Logger instance. */ this._logLevel = defaultLogLevel; /** * The main (internal) log handler for the Logger instance. * Can be set to a new function in internal package code but not by user. */ this._logHandler = defaultLogHandler; /** * The optional, additional, user-defined log handler for the Logger instance. */ this._userLogHandler = null; } Object.defineProperty(Logger.prototype, "logLevel", { get: function () { return this._logLevel; }, set: function (val) { if (!(val in LogLevel)) { throw new TypeError("Invalid value \"" + val + "\" assigned to `logLevel`"); } this._logLevel = val; }, enumerable: false, configurable: true }); // Workaround for setter/getter having to be the same type. Logger.prototype.setLogLevel = function (val) { this._logLevel = typeof val === 'string' ? levelStringToEnum[val] : val; }; Object.defineProperty(Logger.prototype, "logHandler", { get: function () { return this._logHandler; }, set: function (val) { if (typeof val !== 'function') { throw new TypeError('Value assigned to `logHandler` must be a function'); } this._logHandler = val; }, enumerable: false, configurable: true }); Object.defineProperty(Logger.prototype, "userLogHandler", { get: function () { return this._userLogHandler; }, set: function (val) { this._userLogHandler = val; }, enumerable: false, configurable: true }); /** * The functions below are all based on the `console` interface */ Logger.prototype.debug = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } this._userLogHandler && this._userLogHandler.apply(this, __spreadArrays([this, LogLevel.DEBUG], args)); this._logHandler.apply(this, __spreadArrays([this, LogLevel.DEBUG], args)); }; Logger.prototype.log = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } this._userLogHandler && this._userLogHandler.apply(this, __spreadArrays([this, LogLevel.VERBOSE], args)); this._logHandler.apply(this, __spreadArrays([this, LogLevel.VERBOSE], args)); }; Logger.prototype.info = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } this._userLogHandler && this._userLogHandler.apply(this, __spreadArrays([this, LogLevel.INFO], args)); this._logHandler.apply(this, __spreadArrays([this, LogLevel.INFO], args)); }; Logger.prototype.warn = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } this._userLogHandler && this._userLogHandler.apply(this, __spreadArrays([this, LogLevel.WARN], args)); this._logHandler.apply(this, __spreadArrays([this, LogLevel.WARN], args)); }; Logger.prototype.error = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } this._userLogHandler && this._userLogHandler.apply(this, __spreadArrays([this, LogLevel.ERROR], args)); this._logHandler.apply(this, __spreadArrays([this, LogLevel.ERROR], args)); }; return Logger; }()); function isBrowserExtension() { var runtime = typeof chrome === 'object' ? chrome.runtime : typeof browser === 'object' ? browser.runtime : undefined; return typeof runtime === 'object' && runtime.id !== undefined; } /** * This method checks if indexedDB is supported by current browser/service worker context * @return true if indexedDB is supported by current browser/service worker context */ function isIndexedDBAvailable() { return 'indexedDB' in self && indexedDB != null; } /** * This method validates browser context for indexedDB by opening a dummy indexedDB database and reject * if errors occur during the database open operation. */ function validateIndexedDBOpenable() { return new Promise(function (resolve, reject) { try { var preExist_1 = true; var DB_CHECK_NAME_1 = 'validate-browser-context-for-indexeddb-analytics-module'; var request_1 = window.indexedDB.open(DB_CHECK_NAME_1); request_1.onsuccess = function () { request_1.result.close(); // delete database only when it doesn't pre-exist if (!preExist_1) { window.indexedDB.deleteDatabase(DB_CHECK_NAME_1); } resolve(true); }; request_1.onupgradeneeded = function () { preExist_1 = false; }; request_1.onerror = function () { var _a; reject(((_a = request_1.error) === null || _a === void 0 ? void 0 : _a.message) || ''); }; } catch (error) { reject(error); } }); } /** * * This method checks whether cookie is enabled within current browser * @return true if cookie is enabled within current browser */ function areCookiesEnabled() { if (!navigator || !navigator.cookieEnabled) { return false; } return true; } /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var ERROR_NAME = 'FirebaseError'; // Based on code from: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types var FirebaseError = /** @class */ (function (_super) { __extends(FirebaseError, _super); function FirebaseError(code, message, customData) { var _this = _super.call(this, message) || this; _this.code = code; _this.customData = customData; _this.name = ERROR_NAME; // Fix For ES5 // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work Object.setPrototypeOf(_this, FirebaseError.prototype); // Maintains proper stack trace for where our error was thrown. // Only available on V8. if (Error.captureStackTrace) { Error.captureStackTrace(_this, ErrorFactory.prototype.create); } return _this; } return FirebaseError; }(Error)); var ErrorFactory = /** @class */ (function () { function ErrorFactory(service, serviceName, errors) { this.service = service; this.serviceName = serviceName; this.errors = errors; } ErrorFactory.prototype.create = function (code) { var data = []; for (var _i = 1; _i < arguments.length; _i++) { data[_i - 1] = arguments[_i]; } var customData = data[0] || {}; var fullCode = this.service + "/" + code; var template = this.errors[code]; var message = template ? replaceTemplate(template, customData) : 'Error'; // Service Name: Error message (service/code). var fullMessage = this.serviceName + ": " + message + " (" + fullCode + ")."; var error = new FirebaseError(fullCode, fullMessage, customData); return error; }; return ErrorFactory; }()); function replaceTemplate(template, data) { return template.replace(PATTERN, function (_, key) { var value = data[key]; return value != null ? String(value) : "<" + key + "?>"; }); } var PATTERN = /\{\$([^}]+)}/g; /** * @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 amount of milliseconds to exponentially increase. */ var DEFAULT_INTERVAL_MILLIS = 1000; /** * The factor to backoff by. * Should be a number greater than 1. */ var DEFAULT_BACKOFF_FACTOR = 2; /** * The maximum milliseconds to increase to. * *
Visible for testing */ var MAX_VALUE_MILLIS = 4 * 60 * 60 * 1000; // Four hours, like iOS and Android. /** * The percentage of backoff time to randomize by. * See * http://go/safe-client-behavior#step-1-determine-the-appropriate-retry-interval-to-handle-spike-traffic * for context. * *
Visible for testing */ var RANDOM_FACTOR = 0.5; /** * Based on the backoff method from * https://github.com/google/closure-library/blob/master/closure/goog/math/exponentialbackoff.js. * Extracted here so we don't need to pass metadata and a stateful ExponentialBackoff object around. */ function calculateBackoffMillis(backoffCount, intervalMillis, backoffFactor) { if (intervalMillis === void 0) { intervalMillis = DEFAULT_INTERVAL_MILLIS; } if (backoffFactor === void 0) { backoffFactor = DEFAULT_BACKOFF_FACTOR; } // Calculates an exponentially increasing value. // Deviation: calculates value from count and a constant interval, so we only need to save value // and count to restore state. var currBaseValue = intervalMillis * Math.pow(backoffFactor, backoffCount); // A random "fuzz" to avoid waves of retries. // Deviation: randomFactor is required. var randomWait = Math.round( // A fraction of the backoff value to add/subtract. // Deviation: changes multiplication order to improve readability. RANDOM_FACTOR * currBaseValue * // A random float (rounded to int by Math.round above) in the range [-1, 1]. Determines // if we add or subtract. (Math.random() - 0.5) * 2); // Limits backoff to max to avoid effectively permanent backoff. return Math.min(MAX_VALUE_MILLIS, currBaseValue + randomWait); } /** * Component for service name T, e.g. `auth`, `auth-internal` */ var Component = /** @class */ (function () { /** * * @param name The public service name, e.g. app, auth, firestore, database * @param instanceFactory Service factory responsible for creating the public interface * @param type whether the service provided by the component is public or private */ function Component(name, instanceFactory, type) { this.name = name; this.instanceFactory = instanceFactory; this.type = type; this.multipleInstances = false; /** * Properties to be added to the service namespace */ this.serviceProps = {}; this.instantiationMode = "LAZY" /* LAZY */; } Component.prototype.setInstantiationMode = function (mode) { this.instantiationMode = mode; return this; }; Component.prototype.setMultipleInstances = function (multipleInstances) { this.multipleInstances = multipleInstances; return this; }; Component.prototype.setServiceProps = function (props) { this.serviceProps = props; return this; }; return Component; }()); function toArray(arr) { return Array.prototype.slice.call(arr); } function promisifyRequest(request) { return new Promise(function(resolve, reject) { request.onsuccess = function() { resolve(request.result); }; request.onerror = function() { reject(request.error); }; }); } function promisifyRequestCall(obj, method, args) { var request; var p = new Promise(function(resolve, reject) { request = obj[method].apply(obj, args); promisifyRequest(request).then(resolve, reject); }); p.request = request; return p; } function promisifyCursorRequestCall(obj, method, args) { var p = promisifyRequestCall(obj, method, args); return p.then(function(value) { if (!value) return; return new Cursor(value, p.request); }); } function proxyProperties(ProxyClass, targetProp, properties) { properties.forEach(function(prop) { Object.defineProperty(ProxyClass.prototype, prop, { get: function() { return this[targetProp][prop]; }, set: function(val) { this[targetProp][prop] = val; } }); }); } function proxyRequestMethods(ProxyClass, targetProp, Constructor, properties) { properties.forEach(function(prop) { if (!(prop in Constructor.prototype)) return; ProxyClass.prototype[prop] = function() { return promisifyRequestCall(this[targetProp], prop, arguments); }; }); } function proxyMethods(ProxyClass, targetProp, Constructor, properties) { properties.forEach(function(prop) { if (!(prop in Constructor.prototype)) return; ProxyClass.prototype[prop] = function() { return this[targetProp][prop].apply(this[targetProp], arguments); }; }); } function proxyCursorRequestMethods(ProxyClass, targetProp, Constructor, properties) { properties.forEach(function(prop) { if (!(prop in Constructor.prototype)) return; ProxyClass.prototype[prop] = function() { return promisifyCursorRequestCall(this[targetProp], prop, arguments); }; }); } function Index(index) { this._index = index; } proxyProperties(Index, '_index', [ 'name', 'keyPath', 'multiEntry', 'unique' ]); proxyRequestMethods(Index, '_index', IDBIndex, [ 'get', 'getKey', 'getAll', 'getAllKeys', 'count' ]); proxyCursorRequestMethods(Index, '_index', IDBIndex, [ 'openCursor', 'openKeyCursor' ]); function Cursor(cursor, request) { this._cursor = cursor; this._request = request; } proxyProperties(Cursor, '_cursor', [ 'direction', 'key', 'primaryKey', 'value' ]); proxyRequestMethods(Cursor, '_cursor', IDBCursor, [ 'update', 'delete' ]); // proxy 'next' methods ['advance', 'continue', 'continuePrimaryKey'].forEach(function(methodName) { if (!(methodName in IDBCursor.prototype)) return; Cursor.prototype[methodName] = function() { var cursor = this; var args = arguments; return Promise.resolve().then(function() { cursor._cursor[methodName].apply(cursor._cursor, args); return promisifyRequest(cursor._request).then(function(value) { if (!value) return; return new Cursor(value, cursor._request); }); }); }; }); function ObjectStore(store) { this._store = store; } ObjectStore.prototype.createIndex = function() { return new Index(this._store.createIndex.apply(this._store, arguments)); }; ObjectStore.prototype.index = function() { return new Index(this._store.index.apply(this._store, arguments)); }; proxyProperties(ObjectStore, '_store', [ 'name', 'keyPath', 'indexNames', 'autoIncrement' ]); proxyRequestMethods(ObjectStore, '_store', IDBObjectStore, [ 'put', 'add', 'delete', 'clear', 'get', 'getAll', 'getKey', 'getAllKeys', 'count' ]); proxyCursorRequestMethods(ObjectStore, '_store', IDBObjectStore, [ 'openCursor', 'openKeyCursor' ]); proxyMethods(ObjectStore, '_store', IDBObjectStore, [ 'deleteIndex' ]); function Transaction(idbTransaction) { this._tx = idbTransaction; this.complete = new Promise(function(resolve, reject) { idbTransaction.oncomplete = function() { resolve(); }; idbTransaction.onerror = function() { reject(idbTransaction.error); }; idbTransaction.onabort = function() { reject(idbTransaction.error); }; }); } Transaction.prototype.objectStore = function() { return new ObjectStore(this._tx.objectStore.apply(this._tx, arguments)); }; proxyProperties(Transaction, '_tx', [ 'objectStoreNames', 'mode' ]); proxyMethods(Transaction, '_tx', IDBTransaction, [ 'abort' ]); function UpgradeDB(db, oldVersion, transaction) { this._db = db; this.oldVersion = oldVersion; this.transaction = new Transaction(transaction); } UpgradeDB.prototype.createObjectStore = function() { return new ObjectStore(this._db.createObjectStore.apply(this._db, arguments)); }; proxyProperties(UpgradeDB, '_db', [ 'name', 'version', 'objectStoreNames' ]); proxyMethods(UpgradeDB, '_db', IDBDatabase, [ 'deleteObjectStore', 'close' ]); function DB(db) { this._db = db; } DB.prototype.transaction = function() { return new Transaction(this._db.transaction.apply(this._db, arguments)); }; proxyProperties(DB, '_db', [ 'name', 'version', 'objectStoreNames' ]); proxyMethods(DB, '_db', IDBDatabase, [ 'close' ]); // Add cursor iterators // TODO: remove this once browsers do the right thing with promises ['openCursor', 'openKeyCursor'].forEach(function(funcName) { [ObjectStore, Index].forEach(function(Constructor) { // Don't create iterateKeyCursor if openKeyCursor doesn't exist. if (!(funcName in Constructor.prototype)) return; Constructor.prototype[funcName.replace('open', 'iterate')] = function() { var args = toArray(arguments); var callback = args[args.length - 1]; var nativeObject = this._store || this._index; var request = nativeObject[funcName].apply(nativeObject, args.slice(0, -1)); request.onsuccess = function() { callback(request.result); }; }; }); }); // polyfill getAll [Index, ObjectStore].forEach(function(Constructor) { if (Constructor.prototype.getAll) return; Constructor.prototype.getAll = function(query, count) { var instance = this; var items = []; return new Promise(function(resolve) { instance.iterateCursor(query, function(cursor) { if (!cursor) { resolve(items); return; } items.push(cursor.value); if (count !== undefined && items.length == count) { resolve(items); return; } cursor.continue(); }); }); }; }); function openDb(name, version, upgradeCallback) { var p = promisifyRequestCall(indexedDB, 'open', [name, version]); var request = p.request; if (request) { request.onupgradeneeded = function(event) { if (upgradeCallback) { upgradeCallback(new UpgradeDB(request.result, event.oldVersion, request.transaction)); } }; } return p.then(function(db) { return new DB(db); }); } var name = "@firebase/installations-exp"; var version = "0.0.900"; /** * @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. */ var PENDING_TIMEOUT_MS = 10000; var PACKAGE_VERSION = "w:" + version; var INTERNAL_AUTH_VERSION = 'FIS_v2'; var INSTALLATIONS_API_URL = 'https://firebaseinstallations.googleapis.com/v1'; var TOKEN_EXPIRATION_BUFFER = 60 * 60 * 1000; // One hour var SERVICE = 'installations'; var SERVICE_NAME = 'Installations'; /** * @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. */ var _a$1; var ERROR_DESCRIPTION_MAP = (_a$1 = {}, _a$1["missing-app-config-values" /* MISSING_APP_CONFIG_VALUES */] = 'Missing App configuration value: "{$valueName}"', _a$1["not-registered" /* NOT_REGISTERED */] = 'Firebase Installation is not registered.', _a$1["installation-not-found" /* INSTALLATION_NOT_FOUND */] = 'Firebase Installation not found.', _a$1["request-failed" /* REQUEST_FAILED */] = '{$requestName} request failed with error "{$serverCode} {$serverStatus}: {$serverMessage}"', _a$1["app-offline" /* APP_OFFLINE */] = 'Could not process request. Application offline.', _a$1["delete-pending-registration" /* DELETE_PENDING_REGISTRATION */] = "Can't delete installation while there is a pending registration request.", _a$1); var ERROR_FACTORY = new ErrorFactory(SERVICE, SERVICE_NAME, ERROR_DESCRIPTION_MAP); /** Returns true if error is a FirebaseError that is based on an error from the server. */ function isServerError(error) { return (error instanceof FirebaseError && error.code.includes("request-failed" /* REQUEST_FAILED */)); } /** * @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. */ function getInstallationsEndpoint(_a) { var projectId = _a.projectId; return INSTALLATIONS_API_URL + "/projects/" + projectId + "/installations"; } function extractAuthTokenInfoFromResponse(response) { return { token: response.token, requestStatus: 2 /* COMPLETED */, expiresIn: getExpiresInFromResponseExpiresIn(response.expiresIn), creationTime: Date.now() }; } function getErrorFromResponse(requestName, response) { return __awaiter(this, void 0, void 0, function () { var responseJson, errorData; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, response.json()]; case 1: responseJson = _a.sent(); errorData = responseJson.error; return [2 /*return*/, ERROR_FACTORY.create("request-failed" /* REQUEST_FAILED */, { requestName: requestName, serverCode: errorData.code, serverMessage: errorData.message, serverStatus: errorData.status })]; } }); }); } function getHeaders(_a) { var apiKey = _a.apiKey; return new Headers({ 'Content-Type': 'application/json', Accept: 'application/json', 'x-goog-api-key': apiKey }); } function getHeadersWithAuth(appConfig, _a) { var refreshToken = _a.refreshToken; var headers = getHeaders(appConfig); headers.append('Authorization', getAuthorizationHeader(refreshToken)); return headers; } /** * Calls the passed in fetch wrapper and returns the response. * If the returned response has a status of 5xx, re-runs the function once and * returns the response. */ function retryIfServerError(fn) { return __awaiter(this, void 0, void 0, function () { var result; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, fn()]; case 1: result = _a.sent(); if (result.status >= 500 && result.status < 600) { // Internal Server Error. Retry request. return [2 /*return*/, fn()]; } return [2 /*return*/, result]; } }); }); } function getExpiresInFromResponseExpiresIn(responseExpiresIn) { // This works because the server will never respond with fractions of a second. return Number(responseExpiresIn.replace('s', '000')); } function getAuthorizationHeader(refreshToken) { return INTERNAL_AUTH_VERSION + " " + refreshToken; } /** * @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. */ function createInstallationRequest(appConfig, _a) { var fid = _a.fid; return __awaiter(this, void 0, void 0, function () { var endpoint, headers, body, request, response, responseValue, registeredInstallationEntry; return __generator(this, function (_b) { switch (_b.label) { case 0: endpoint = getInstallationsEndpoint(appConfig); headers = getHeaders(appConfig); body = { fid: fid, authVersion: INTERNAL_AUTH_VERSION, appId: appConfig.appId, sdkVersion: PACKAGE_VERSION }; request = { method: 'POST', headers: headers, body: JSON.stringify(body) }; return [4 /*yield*/, retryIfServerError(function () { return fetch(endpoint, request); })]; case 1: response = _b.sent(); if (!response.ok) return [3 /*break*/, 3]; return [4 /*yield*/, response.json()]; case 2: responseValue = _b.sent(); registeredInstallationEntry = { fid: responseValue.fid || fid, registrationStatus: 2 /* COMPLETED */, refreshToken: responseValue.refreshToken, authToken: extractAuthTokenInfoFromResponse(responseValue.authToken) }; return [2 /*return*/, registeredInstallationEntry]; case 3: return [4 /*yield*/, getErrorFromResponse('Create Installation', response)]; case 4: throw _b.sent(); } }); }); } /** * @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. */ /** Returns a promise that resolves after given time passes. */ function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } /** * @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. */ function bufferToBase64UrlSafe(array) { var b64 = btoa(String.fromCharCode.apply(String, __spread(array))); return b64.replace(/\+/g, '-').replace(/\//g, '_'); } /** * @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. */ var VALID_FID_PATTERN = /^[cdef][\w-]{21}$/; var INVALID_FID = ''; /** * Generates a new FID using random values from Web Crypto API. * Returns an empty string if FID generation fails for any reason. */ function generateFid() { try { // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 // bytes. our implementation generates a 17 byte array instead. var fidByteArray = new Uint8Array(17); var crypto_1 = self.crypto || self.msCrypto; crypto_1.getRandomValues(fidByteArray); // Replace the first 4 random bits with the constant FID header of 0b0111. fidByteArray[0] = 112 + (fidByteArray[0] % 16); var fid = encode(fidByteArray); return VALID_FID_PATTERN.test(fid) ? fid : INVALID_FID; } catch (_a) { // FID generation errored return INVALID_FID; } } /** Converts a FID Uint8Array to a base64 string representation. */ function encode(fidByteArray) { var b64String = bufferToBase64UrlSafe(fidByteArray); // Remove the 23rd character that was added because of the extra 4 bits at the // end of our 17 byte array, and the '=' padding. return b64String.substr(0, 22); } /** * @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. */ /** Returns a string key that can be used to identify the app. */ function getKey(appConfig) { return appConfig.appName + "!" + appConfig.appId; } /** * @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. */ var fidChangeCallbacks = new Map(); /** * Calls the onIdChange callbacks with the new FID value, and broadcasts the * change to other tabs. */ function fidChanged(appConfig, fid) { var key = getKey(appConfig); callFidChangeCallbacks(key, fid); broadcastFidChange(key, fid); } function callFidChangeCallbacks(key, fid) { var e_1, _a; var callbacks = fidChangeCallbacks.get(key); if (!callbacks) { return; } try { for (var callbacks_1 = __values(callbacks), callbacks_1_1 = callbacks_1.next(); !callbacks_1_1.done; callbacks_1_1 = callbacks_1.next()) { var callback = callbacks_1_1.value; callback(fid); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (callbacks_1_1 && !callbacks_1_1.done && (_a = callbacks_1.return)) _a.call(callbacks_1); } finally { if (e_1) throw e_1.error; } } } function broadcastFidChange(key, fid) { var channel = getBroadcastChannel(); if (channel) { channel.postMessage({ key: key, fid: fid }); } closeBroadcastChannel(); } var broadcastChannel = null; /** Opens and returns a BroadcastChannel if it is supported by the browser. */ function getBroadcastChannel() { if (!broadcastChannel && 'BroadcastChannel' in self) { broadcastChannel = new BroadcastChannel('[Firebase] FID Change'); broadcastChannel.onmessage = function (e) { callFidChangeCallbacks(e.data.key, e.data.fid); }; } return broadcastChannel; } function closeBroadcastChannel() { if (fidChangeCallbacks.size === 0 && broadcastChannel) { broadcastChannel.close(); broadcastChannel = null; } } /** * @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. */ var DATABASE_NAME = 'firebase-installations-database'; var DATABASE_VERSION = 1; var OBJECT_STORE_NAME = 'firebase-installations-store'; var dbPromise = null; function getDbPromise() { if (!dbPromise) { dbPromise = openDb(DATABASE_NAME, DATABASE_VERSION, function (upgradeDB) { // We don't use 'break' in this switch statement, the fall-through // behavior is what we want, because if there are multiple versions between // the old version and the current version, we want ALL the migrations // that correspond to those versions to run, not only the last one. // eslint-disable-next-line default-case switch (upgradeDB.oldVersion) { case 0: upgradeDB.createObjectStore(OBJECT_STORE_NAME); } }); } return dbPromise; } /** Assigns or overwrites the record for the given key with the given value. */ function set(appConfig, value) { return __awaiter(this, void 0, void 0, function () { var key, db, tx, objectStore, oldValue; return __generator(this, function (_a) { switch (_a.label) { case 0: key = getKey(appConfig); return [4 /*yield*/, getDbPromise()]; case 1: db = _a.sent(); tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); objectStore = tx.objectStore(OBJECT_STORE_NAME); return [4 /*yield*/, objectStore.get(key)]; case 2: oldValue = _a.sent(); return [4 /*yield*/, objectStore.put(value, key)]; case 3: _a.sent(); return [4 /*yield*/, tx.complete]; case 4: _a.sent(); if (!oldValue || oldValue.fid !== value.fid) { fidChanged(appConfig, value.fid); } return [2 /*return*/, value]; } }); }); } /** Removes record(s) from the objectStore that match the given key. */ function remove(appConfig) { return __awaiter(this, void 0, void 0, function () { var key, db, tx; return __generator(this, function (_a) { switch (_a.label) { case 0: key = getKey(appConfig); return [4 /*yield*/, getDbPromise()]; case 1: db = _a.sent(); tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); return [4 /*yield*/, tx.objectStore(OBJECT_STORE_NAME).delete(key)]; case 2: _a.sent(); return [4 /*yield*/, tx.complete]; case 3: _a.sent(); return [2 /*return*/]; } }); }); } /** * Atomically updates a record with the result of updateFn, which gets * called with the current value. If newValue is undefined, the record is * deleted instead. * @return Updated value */ function update(appConfig, updateFn) { return __awaiter(this, void 0, void 0, function () { var key, db, tx, store, oldValue, newValue; return __generator(this, function (_a) { switch (_a.label) { case 0: key = getKey(appConfig); return [4 /*yield*/, getDbPromise()]; case 1: db = _a.sent(); tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); store = tx.objectStore(OBJECT_STORE_NAME); return [4 /*yield*/, store.get(key)]; case 2: oldValue = _a.sent(); newValue = updateFn(oldValue); if (!(newValue === undefined)) return [3 /*break*/, 4]; return [4 /*yield*/, store.delete(key)]; case 3: _a.sent(); return [3 /*break*/, 6]; case 4: return [4 /*yield*/, store.put(newValue, key)]; case 5: _a.sent(); _a.label = 6; case 6: return [4 /*yield*/, tx.complete]; case 7: _a.sent(); if (newValue && (!oldValue || oldValue.fid !== newValue.fid)) { fidChanged(appConfig, newValue.fid); } return [2 /*return*/, newValue]; } }); }); } /** * @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. */ /** * Updates and returns the InstallationEntry from the database. * Also triggers a registration request if it is necessary and possible. */ function getInstallationEntry(appConfig) { return __awaiter(this, void 0, void 0, function () { var registrationPromise, installationEntry; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, update(appConfig, function (oldEntry) { var installationEntry = updateOrCreateInstallationEntry(oldEntry); var entryWithPromise = triggerRegistrationIfNecessary(appConfig, installationEntry); registrationPromise = entryWithPromise.registrationPromise; return entryWithPromise.installationEntry; })]; case 1: installationEntry = _b.sent(); if (!(installationEntry.fid === INVALID_FID)) return [3 /*break*/, 3]; _a = {}; return [4 /*yield*/, registrationPromise]; case 2: // FID generation failed. Waiting for the FID from the server. return [2 /*return*/, (_a.installationEntry = _b.sent(), _a)]; case 3: return [2 /*return*/, { installationEntry: installationEntry, registrationPromise: registrationPromise }]; } }); }); } /** * Creates a new Installation Entry if one does not exist. * Also clears timed out pending requests. */ function updateOrCreateInstallationEntry(oldEntry) { var entry = oldEntry || { fid: generateFid(), registrationStatus: 0 /* NOT_STARTED */ }; return clearTimedOutRequest(entry); } /** * If the Firebase Installation is not registered yet, this will trigger the * registration and return an InProgressInstallationEntry. * * If registrationPromise does not exist, the installationEntry is guaranteed * to be registered. */ function triggerRegistrationIfNecessary(appConfig, installationEntry) { if (installationEntry.registrationStatus === 0 /* NOT_STARTED */) { if (!navigator.onLine) { // Registration required but app is offline. var registrationPromiseWithError = Promise.reject(ERROR_FACTORY.create("app-offline" /* APP_OFFLINE */)); return { installationEntry: installationEntry, registrationPromise: registrationPromiseWithError }; } // Try registering. Change status to IN_PROGRESS. var inProgressEntry = { fid: installationEntry.fid, registrationStatus: 1 /* IN_PROGRESS */, registrationTime: Date.now() }; var registrationPromise = registerInstallation(appConfig, inProgressEntry); return { installationEntry: inProgressEntry, registrationPromise: registrationPromise }; } else if (installationEntry.registrationStatus === 1 /* IN_PROGRESS */) { return { installationEntry: installationEntry, registrationPromise: waitUntilFidRegistration(appConfig) }; } else { return { installationEntry: installationEntry }; } } /** This will be executed only once for each new Firebase Installation. */ function registerInstallation(appConfig, installationEntry) { return __awaiter(this, void 0, void 0, function () { var registeredInstallationEntry, e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 7]); return [4 /*yield*/, createInstallationRequest(appConfig, installationEntry)]; case 1: registeredInstallationEntry = _a.sent(); return [2 /*return*/, set(appConfig, registeredInstallationEntry)]; case 2: e_1 = _a.sent(); if (!(isServerError(e_1) && e_1.customData.serverCode === 409)) return [3 /*break*/, 4]; // Server returned a "FID can not be used" error. // Generate a new ID next time. return [4 /*yield*/, remove(appConfig)]; case 3: // Server returned a "FID can not be used" error. // Generate a new ID next time. _a.sent(); return [3 /*break*/, 6]; case 4: // Registration failed. Set FID as not registered. return [4 /*yield*/, set(appConfig, { fid: installationEntry.fid, registrationStatus: 0 /* NOT_STARTED */ })]; case 5: // Registration failed. Set FID as not registered. _a.sent(); _a.label = 6; case 6: throw e_1; case 7: return [2 /*return*/]; } }); }); } /** Call if FID registration is pending in another request. */ function waitUntilFidRegistration(appConfig) { return __awaiter(this, void 0, void 0, function () { var entry, _a, installationEntry, registrationPromise; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, updateInstallationRequest(appConfig)]; case 1: entry = _b.sent(); _b.label = 2; case 2: if (!(entry.registrationStatus === 1 /* IN_PROGRESS */)) return [3 /*break*/, 5]; // createInstallation request still in progress. return [4 /*yield*/, sleep(100)]; case 3: // createInstallation request still in progress. _b.sent(); return [4 /*yield*/, updateInstallationRequest(appConfig)]; case 4: entry = _b.sent(); return [3 /*break*/, 2]; case 5: if (!(entry.registrationStatus === 0 /* NOT_STARTED */)) return [3 /*break*/, 7]; return [4 /*yield*/, getInstallationEntry(appConfig)]; case 6: _a = _b.sent(), installationEntry = _a.installationEntry, registrationPromise = _a.registrationPromise; if (registrationPromise) { return [2 /*return*/, registrationPromise]; } else { // if there is no registrationPromise, entry is registered. return [2 /*return*/, installationEntry]; } case 7: return [2 /*return*/, entry]; } }); }); } /** * Called only if there is a CreateInstallation request in progress. * * Updates the InstallationEntry in the DB based on the status of the * CreateInstallation request. * * Returns the updated InstallationEntry. */ function updateInstallationRequest(appConfig) { return update(appConfig, function (oldEntry) { if (!oldEntry) { throw ERROR_FACTORY.create("installation-not-found" /* INSTALLATION_NOT_FOUND */); } return clearTimedOutRequest(oldEntry); }); } function clearTimedOutRequest(entry) { if (hasInstallationRequestTimedOut(entry)) { return { fid: entry.fid, registrationStatus: 0 /* NOT_STARTED */ }; } return entry; } function hasInstallationRequestTimedOut(installationEntry) { return (installationEntry.registrationStatus === 1 /* IN_PROGRESS */ && installationEntry.registrationTime + PENDING_TIMEOUT_MS < Date.now()); } /** * @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. */ function generateAuthTokenRequest(_a, installationEntry) { var appConfig = _a.appConfig, platformLoggerProvider = _a.platformLoggerProvider; return __awaiter(this, void 0, void 0, function () { var endpoint, headers, platformLogger, body, request, response, responseValue, completedAuthToken; return __generator(this, function (_b) { switch (_b.label) { case 0: endpoint = getGenerateAuthTokenEndpoint(appConfig, installationEntry); headers = getHeadersWithAuth(appConfig, installationEntry); platformLogger = platformLoggerProvider.getImmediate({ optional: true }); if (platformLogger) { headers.append('x-firebase-client', platformLogger.getPlatformInfoString()); } body = { installation: { sdkVersion: PACKAGE_VERSION } }; request = { method: 'POST', headers: headers, body: JSON.stringify(body) }; return [4 /*yield*/, retryIfServerError(function () { return fetch(endpoint, request); })]; case 1: response = _b.sent(); if (!response.ok) return [3 /*break*/, 3]; return [4 /*yield*/, response.json()]; case 2: responseValue = _b.sent(); completedAuthToken = extractAuthTokenInfoFromResponse(responseValue); return [2 /*return*/, completedAuthToken]; case 3: return [4 /*yield*/, getErrorFromResponse('Generate Auth Token', response)]; case 4: throw _b.sent(); } }); }); } function getGenerateAuthTokenEndpoint(appConfig, _a) { var fid = _a.fid; return getInstallationsEndpoint(appConfig) + "/" + fid + "/authTokens:generate"; } /** * @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. */ /** * Returns a valid authentication token for the installation. Generates a new * token if one doesn't exist, is expired or about to expire. * * Should only be called if the Firebase Installation is registered. */ function refreshAuthToken(installations, forceRefresh) { if (forceRefresh === void 0) { forceRefresh = false; } return __awaiter(this, void 0, void 0, function () { var tokenPromise, entry, authToken, _a; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, update(installations.appConfig, function (oldEntry) { if (!isEntryRegistered(oldEntry)) { throw ERROR_FACTORY.create("not-registered" /* NOT_REGISTERED */); } var oldAuthToken = oldEntry.authToken; if (!forceRefresh && isAuthTokenValid(oldAuthToken)) { // There is a valid token in the DB. return oldEntry; } else if (oldAuthToken.requestStatus === 1 /* IN_PROGRESS */) { // There already is a token request in progress. tokenPromise = waitUntilAuthTokenRequest(installations, forceRefresh); return oldEntry; } else { // No token or token expired. if (!navigator.onLine) { throw ERROR_FACTORY.create("app-offline" /* APP_OFFLINE */); } var inProgressEntry = makeAuthTokenRequestInProgressEntry(oldEntry); tokenPromise = fetchAuthTokenFromServer(installations, inProgressEntry); return inProgressEntry; } })]; case 1: entry = _b.sent(); if (!tokenPromise) return [3 /*break*/, 3]; return [4 /*yield*/, tokenPromise]; case 2: _a = _b.sent(); return [3 /*break*/, 4]; case 3: _a = entry.authToken; _b.label = 4; case 4: authToken = _a; return [2 /*return*/, authToken]; } }); }); } /** * Call only if FID is registered and Auth Token request is in progress. * * Waits until the current pending request finishes. If the request times out, * tries once in this thread as well. */ function waitUntilAuthTokenRequest(installations, forceRefresh) { return __awaiter(this, void 0, void 0, function () { var entry, authToken; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, updateAuthTokenRequest(installations.appConfig)]; case 1: entry = _a.sent(); _a.label = 2; case 2: if (!(entry.authToken.requestStatus === 1 /* IN_PROGRESS */)) return [3 /*break*/, 5]; // generateAuthToken still in progress. return [4 /*yield*/, sleep(100)]; case 3: // generateAuthToken still in progress. _a.sent(); return [4 /*yield*/, updateAuthTokenRequest(installations.appConfig)]; case 4: entry = _a.sent(); return [3 /*break*/, 2]; case 5: authToken = entry.authToken; if (authToken.requestStatus === 0 /* NOT_STARTED */) { // The request timed out or failed in a different call. Try again. return [2 /*return*/, refreshAuthToken(installations, forceRefresh)]; } else { return [2 /*return*/, authToken]; } } }); }); } /** * Called only if there is a GenerateAuthToken request in progress. * * Updates the InstallationEntry in the DB based on the status of the * GenerateAuthToken request. * * Returns the updated InstallationEntry. */ function updateAuthTokenRequest(appConfig) { return update(appConfig, function (oldEntry) { if (!isEntryRegistered(oldEntry)) { throw ERROR_FACTORY.create("not-registered" /* NOT_REGISTERED */); } var oldAuthToken = oldEntry.authToken; if (hasAuthTokenRequestTimedOut(oldAuthToken)) { return __assign(__assign({}, oldEntry), { authToken: { requestStatus: 0 /* NOT_STARTED */ } }); } return oldEntry; }); } function fetchAuthTokenFromServer(installations, installationEntry) { return __awaiter(this, void 0, void 0, function () { var authToken, updatedInstallationEntry, e_1, updatedInstallationEntry; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 3, , 8]); return [4 /*yield*/, generateAuthTokenRequest(installations, installationEntry)]; case 1: authToken = _a.sent(); updatedInstallationEntry = __assign(__assign({}, installationEntry), { authToken: authToken }); return [4 /*yield*/, set(installations.appConfig, updatedInstallationEntry)]; case 2: _a.sent(); return [2 /*return*/, authToken]; case 3: e_1 = _a.sent(); if (!(isServerError(e_1) && (e_1.customData.serverCode === 401 || e_1.customData.serverCode === 404))) return [3 /*break*/, 5]; // Server returned a "FID not found" or a "Invalid authentication" error. // Generate a new ID next time. return [4 /*yield*/, remove(installations.appConfig)]; case 4: // Server returned a "FID not found" or a "Invalid authentication" error. // Generate a new ID next time. _a.sent(); return [3 /*break*/, 7]; case 5: updatedInstallationEntry = __assign(__assign({}, installationEntry), { authToken: { requestStatus: 0 /* NOT_STARTED */ } }); return [4 /*yield*/, set(installations.appConfig, updatedInstallationEntry)]; case 6: _a.sent(); _a.label = 7; case 7: throw e_1; case 8: return [2 /*return*/]; } }); }); } function isEntryRegistered(installationEntry) { return (installationEntry !== undefined && installationEntry.registrationStatus === 2 /* COMPLETED */); } function isAuthTokenValid(authToken) { return (authToken.requestStatus === 2 /* COMPLETED */ && !isAuthTokenExpired(authToken)); } function isAuthTokenExpired(authToken) { var now = Date.now(); return (now < authToken.creationTime || authToken.creationTime + authToken.expiresIn < now + TOKEN_EXPIRATION_BUFFER); } /** Returns an updated InstallationEntry with an InProgressAuthToken. */ function makeAuthTokenRequestInProgressEntry(oldEntry) { var inProgressAuthToken = { requestStatus: 1 /* IN_PROGRESS */, requestTime: Date.now() }; return __assign(__assign({}, oldEntry), { authToken: inProgressAuthToken }); } function hasAuthTokenRequestTimedOut(authToken) { return (authToken.requestStatus === 1 /* IN_PROGRESS */ && authToken.requestTime + PENDING_TIMEOUT_MS < Date.now()); } /** * @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. */ /** * Creates a Firebase Installation if there isn't one for the app and * returns the Installation ID. * * @public */ function getId(installations) { return __awaiter(this, void 0, void 0, function () { var installationsImpl, _a, installationEntry, registrationPromise; return __generator(this, function (_b) { switch (_b.label) { case 0: installationsImpl = installations; return [4 /*yield*/, getInstallationEntry(installationsImpl.appConfig)]; case 1: _a = _b.sent(), installationEntry = _a.installationEntry, registrationPromise = _a.registrationPromise; if (registrationPromise) { registrationPromise.catch(console.error); } else { // If the installation is already registered, update the authentication // token if needed. refreshAuthToken(installationsImpl).catch(console.error); } return [2 /*return*/, installationEntry.fid]; } }); }); } /** * @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. */ /** * Returns an Installation auth token, identifying the current Firebase Installation. * * @public */ function getToken(installations, forceRefresh) { if (forceRefresh === void 0) { forceRefresh = false; } return __awaiter(this, void 0, void 0, function () { var installationsImpl, authToken; return __generator(this, function (_a) { switch (_a.label) { case 0: installationsImpl = installations; return [4 /*yield*/, completeInstallationRegistration(installationsImpl.appConfig)]; case 1: _a.sent(); return [4 /*yield*/, refreshAuthToken(installationsImpl, forceRefresh)]; case 2: authToken = _a.sent(); return [2 /*return*/, authToken.token]; } }); }); } function completeInstallationRegistration(appConfig) { return __awaiter(this, void 0, void 0, function () { var registrationPromise; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, getInstallationEntry(appConfig)]; case 1: registrationPromise = (_a.sent()).registrationPromise; if (!registrationPromise) return [3 /*break*/, 3]; // A createInstallation request is in progress. Wait until it finishes. return [4 /*yield*/, registrationPromise]; case 2: // A createInstallation request is in progress. Wait until it finishes. _a.sent(); _a.label = 3; case 3: return [2 /*return*/]; } }); }); } /** * @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. */ function extractAppConfig(app) { var e_1, _a; if (!app || !app.options) { throw getMissingValueError('App Configuration'); } if (!app.name) { throw getMissingValueError('App Name'); } // Required app config keys var configKeys = [ 'projectId', 'apiKey', 'appId' ]; try { for (var configKeys_1 = __values(configKeys), configKeys_1_1 = configKeys_1.next(); !configKeys_1_1.done; configKeys_1_1 = configKeys_1.next()) { var keyName = configKeys_1_1.value; if (!app.options[keyName]) { throw getMissingValueError(keyName); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (configKeys_1_1 && !configKeys_1_1.done && (_a = configKeys_1.return)) _a.call(configKeys_1); } finally { if (e_1) throw e_1.error; } } return { appName: app.name, projectId: app.options.projectId, apiKey: app.options.apiKey, appId: app.options.appId }; } function getMissingValueError(valueName) { return ERROR_FACTORY.create("missing-app-config-values" /* MISSING_APP_CONFIG_VALUES */, { valueName: valueName }); } /** * @license * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var INSTALLATIONS_NAME = 'installations-exp'; var INSTALLATIONS_NAME_INTERNAL = 'installations-exp-internal'; var publicFactory = function (container) { var app$1 = container.getProvider('app-exp').getImmediate(); // Throws if app isn't configured properly. var appConfig = extractAppConfig(app$1); var platformLoggerProvider = app._getProvider(app$1, 'platform-logger'); var installationsImpl = { app: app$1, appConfig: appConfig, platformLoggerProvider: platformLoggerProvider, _delete: function () { return Promise.resolve(); } }; return installationsImpl; }; var internalFactory = function (container) { var app$1 = container.getProvider('app-exp').getImmediate(); // Internal FIS instance relies on public FIS instance. var installations = app._getProvider(app$1, INSTALLATIONS_NAME).getImmediate(); var installationsInternal = { getId: function () { return getId(installations); }, getToken: function (forceRefresh) { return getToken(installations, forceRefresh); } }; return installationsInternal; }; function registerInstallations() { app._registerComponent(new Component(INSTALLATIONS_NAME, publicFactory, "PUBLIC" /* PUBLIC */)); app._registerComponent(new Component(INSTALLATIONS_NAME_INTERNAL, internalFactory, "PRIVATE" /* PRIVATE */)); } /** * @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. */ registerInstallations(); app.registerVersion(name, version); /** * @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. */ /** * Type constant for Firebase Analytics. */ var ANALYTICS_TYPE = 'analytics-exp'; // Key to attach FID to in gtag params. var GA_FID_KEY = 'firebase_id'; var ORIGIN_KEY = 'origin'; var FETCH_TIMEOUT_MILLIS = 60 * 1000; var DYNAMIC_CONFIG_URL = 'https://firebase.googleapis.com/v1alpha/projects/-/apps/{app-id}/webConfig'; var GTAG_URL = 'https://www.googletagmanager.com/gtag/js'; /** * @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. */ var logger = new Logger('@firebase/analytics'); /** * @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. */ /** * Makeshift polyfill for Promise.allSettled(). Resolves when all promises * have either resolved or rejected. * * @param promises Array of promises to wait for. */ function promiseAllSettled(promises) { return Promise.all(promises.map(function (promise) { return promise.catch(function (e) { return e; }); })); } /** * Inserts gtag script tag into the page to asynchronously download gtag. * @param dataLayerName Name of datalayer (most often the default, "_dataLayer"). */ function insertScriptTag(dataLayerName, measurementId) { var script = document.createElement('script'); // We are not providing an analyticsId in the URL because it would trigger a `page_view` // without fid. We will initialize ga-id using gtag (config) command together with fid. script.src = GTAG_URL + "?l=" + dataLayerName + "&id=" + measurementId; script.async = true; document.head.appendChild(script); } /** * Get reference to, or create, global datalayer. * @param dataLayerName Name of datalayer (most often the default, "_dataLayer"). */ function getOrCreateDataLayer(dataLayerName) { // Check for existing dataLayer and create if needed. var dataLayer = []; if (Array.isArray(window[dataLayerName])) { dataLayer = window[dataLayerName]; } else { window[dataLayerName] = dataLayer; } return dataLayer; } /** * Wrapped gtag logic when gtag is called with 'config' command. * * @param gtagCore Basic gtag function that just appends to dataLayer. * @param initializationPromisesMap Map of appIds to their initialization promises. * @param dynamicConfigPromisesList Array of dynamic config fetch promises. * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId. * @param measurementId GA Measurement ID to set config for. * @param gtagParams Gtag config params to set. */ function gtagOnConfig(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, measurementId, gtagParams) { return __awaiter(this, void 0, void 0, function () { var correspondingAppId, dynamicConfigResults, foundConfig, e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: correspondingAppId = measurementIdToAppId[measurementId]; _a.label = 1; case 1: _a.trys.push([1, 7, , 8]); if (!correspondingAppId) return [3 /*break*/, 3]; return [4 /*yield*/, initializationPromisesMap[correspondingAppId]]; case 2: _a.sent(); return [3 /*break*/, 6]; case 3: return [4 /*yield*/, promiseAllSettled(dynamicConfigPromisesList)]; case 4: dynamicConfigResults = _a.sent(); foundConfig = dynamicConfigResults.find(function (config) { return config.measurementId === measurementId; }); if (!foundConfig) return [3 /*break*/, 6]; return [4 /*yield*/, initializationPromisesMap[foundConfig.appId]]; case 5: _a.sent(); _a.label = 6; case 6: return [3 /*break*/, 8]; case 7: e_1 = _a.sent(); logger.error(e_1); return [3 /*break*/, 8]; case 8: gtagCore("config" /* CONFIG */, measurementId, gtagParams); return [2 /*return*/]; } }); }); } /** * Wrapped gtag logic when gtag is called with 'event' command. * * @param gtagCore Basic gtag function that just appends to dataLayer. * @param initializationPromisesMap Map of appIds to their initialization promises. * @param dynamicConfigPromisesList Array of dynamic config fetch promises. * @param measurementId GA Measurement ID to log event to. * @param gtagParams Params to log with this event. */ function gtagOnEvent(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementId, gtagParams) { return __awaiter(this, void 0, void 0, function () { var initializationPromisesToWaitFor, gaSendToList, dynamicConfigResults, _loop_1, _i, gaSendToList_1, sendToId, state_1, e_2; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 4, , 5]); initializationPromisesToWaitFor = []; if (!(gtagParams && gtagParams['send_to'])) return [3 /*break*/, 2]; gaSendToList = gtagParams['send_to']; // Make it an array if is isn't, so it can be dealt with the same way. if (!Array.isArray(gaSendToList)) { gaSendToList = [gaSendToList]; } return [4 /*yield*/, promiseAllSettled(dynamicConfigPromisesList)]; case 1: dynamicConfigResults = _a.sent(); _loop_1 = function (sendToId) { // Any fetched dynamic measurement ID that matches this 'send_to' ID var foundConfig = dynamicConfigResults.find(function (config) { return config.measurementId === sendToId; }); var initializationPromise = foundConfig && initializationPromisesMap[foundConfig.appId]; if (initializationPromise) { initializationPromisesToWaitFor.push(initializationPromise); } else { // Found an item in 'send_to' that is not associated // directly with an FID, possibly a group. Empty this array, // exit the loop early, and let it get populated below. initializationPromisesToWaitFor = []; return "break"; } }; for (_i = 0, gaSendToList_1 = gaSendToList; _i < gaSendToList_1.length; _i++) { sendToId = gaSendToList_1[_i]; state_1 = _loop_1(sendToId); if (state_1 === "break") break; } _a.label = 2; case 2: // This will be unpopulated if there was no 'send_to' field , or // if not all entries in the 'send_to' field could be mapped to // a FID. In these cases, wait on all pending initialization promises. if (initializationPromisesToWaitFor.length === 0) { initializationPromisesToWaitFor = Object.values(initializationPromisesMap); } // Run core gtag function with args after all relevant initialization // promises have been resolved. return [4 /*yield*/, Promise.all(initializationPromisesToWaitFor)]; case 3: // Run core gtag function with args after all relevant initialization // promises have been resolved. _a.sent(); // Workaround for http://b/141370449 - third argument cannot be undefined. gtagCore("event" /* EVENT */, measurementId, gtagParams || {}); return [3 /*break*/, 5]; case 4: e_2 = _a.sent(); logger.error(e_2); return [3 /*break*/, 5]; case 5: return [2 /*return*/]; } }); }); } /** * Wraps a standard gtag function with extra code to wait for completion of * relevant initialization promises before sending requests. * * @param gtagCore Basic gtag function that just appends to dataLayer. * @param initializationPromisesMap Map of appIds to their initialization promises. * @param dynamicConfigPromisesList Array of dynamic config fetch promises. * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId. */ function wrapGtag(gtagCore, /** * Allows wrapped gtag calls to wait on whichever intialization promises are required, * depending on the contents of the gtag params' `send_to` field, if any. */ initializationPromisesMap, /** * Wrapped gtag calls sometimes require all dynamic config fetches to have returned * before determining what initialization promises (which include FIDs) to wait for. */ dynamicConfigPromisesList, /** * Wrapped gtag config calls can narrow down which initialization promise (with FID) * to wait for if the measurementId is already fetched, by getting the corresponding appId, * which is the key for the initialization promises map. */ measurementIdToAppId) { /** * Wrapper around gtag that ensures FID is sent with gtag calls. * @param command Gtag command type. * @param idOrNameOrParams Measurement ID if command is EVENT/CONFIG, params if command is SET. * @param gtagParams Params if event is EVENT/CONFIG. */ function gtagWrapper(command, idOrNameOrParams, gtagParams) { return __awaiter(this, void 0, void 0, function () { var e_3; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 6, , 7]); if (!(command === "event" /* EVENT */)) return [3 /*break*/, 2]; // If EVENT, second arg must be measurementId. return [4 /*yield*/, gtagOnEvent(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, idOrNameOrParams, gtagParams)]; case 1: // If EVENT, second arg must be measurementId. _a.sent(); return [3 /*break*/, 5]; case 2: if (!(command === "config" /* CONFIG */)) return [3 /*break*/, 4]; // If CONFIG, second arg must be measurementId. return [4 /*yield*/, gtagOnConfig(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, idOrNameOrParams, gtagParams)]; case 3: // If CONFIG, second arg must be measurementId. _a.sent(); return [3 /*break*/, 5]; case 4: // If SET, second arg must be params. gtagCore("set" /* SET */, idOrNameOrParams); _a.label = 5; case 5: return [3 /*break*/, 7]; case 6: e_3 = _a.sent(); logger.error(e_3); return [3 /*break*/, 7]; case 7: return [2 /*return*/]; } }); }); } return gtagWrapper; } /** * Creates global gtag function or wraps existing one if found. * This wrapped function attaches Firebase instance ID (FID) to gtag 'config' and * 'event' calls that belong to the GAID associated with this Firebase instance. * * @param initializationPromisesMap Map of appIds to their initialization promises. * @param dynamicConfigPromisesList Array of dynamic config fetch promises. * @param measurementIdToAppId Map of GA measurementIDs to corresponding Firebase appId. * @param dataLayerName Name of global GA datalayer array. * @param gtagFunctionName Name of global gtag function ("gtag" if not user-specified). */ function wrapOrCreateGtag(initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId, dataLayerName, gtagFunctionName) { // Create a basic core gtag function var gtagCore = function () { var _args = []; for (var _i = 0; _i < arguments.length; _i++) { _args[_i] = arguments[_i]; } // Must push IArguments object, not an array. window[dataLayerName].push(arguments); }; // Replace it with existing one if found if (window[gtagFunctionName] && typeof window[gtagFunctionName] === 'function') { // @ts-ignore gtagCore = window[gtagFunctionName]; } window[gtagFunctionName] = wrapGtag(gtagCore, initializationPromisesMap, dynamicConfigPromisesList, measurementIdToAppId); return { gtagCore: gtagCore, wrappedGtag: window[gtagFunctionName] }; } /** * Returns first script tag in DOM matching our gtag url pattern. */ function findGtagScriptOnPage() { var scriptTags = window.document.getElementsByTagName('script'); for (var _i = 0, _a = Object.values(scriptTags); _i < _a.length; _i++) { var tag = _a[_i]; if (tag.src && tag.src.includes(GTAG_URL)) { return tag; } } return null; } /** * @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. */ var _a$2; var ERRORS = (_a$2 = {}, _a$2["already-exists" /* ALREADY_EXISTS */] = 'A Firebase Analytics instance with the appId {$id} ' + ' already exists. ' + 'Only one Firebase Analytics instance can be created for each appId.', _a$2["already-initialized" /* ALREADY_INITIALIZED */] = 'Firebase Analytics has already been initialized.' + 'settings() must be called before initializing any Analytics instance' + 'or it will have no effect.', _a$2["interop-component-reg-failed" /* INTEROP_COMPONENT_REG_FAILED */] = 'Firebase Analytics Interop Component failed to instantiate: {$reason}', _a$2["invalid-analytics-context" /* INVALID_ANALYTICS_CONTEXT */] = 'Firebase Analytics is not supported in this environment. ' + 'Wrap initialization of analytics in analytics.isSupported() ' + 'to prevent initialization in unsupported environments. Details: {$errorInfo}', _a$2["indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */] = 'IndexedDB unavailable or restricted in this environment. ' + 'Wrap initialization of analytics in analytics.isSupported() ' + 'to prevent initialization in unsupported environments. Details: {$errorInfo}', _a$2["fetch-throttle" /* FETCH_THROTTLE */] = 'The config fetch request timed out while in an exponential backoff state.' + ' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.', _a$2["config-fetch-failed" /* CONFIG_FETCH_FAILED */] = 'Dynamic config fetch failed: [{$httpStatus}] {$responseMessage}', _a$2["no-api-key" /* NO_API_KEY */] = 'The "apiKey" field is empty in the local Firebase config. Firebase Analytics requires this field to' + 'contain a valid API key.', _a$2["no-app-id" /* NO_APP_ID */] = 'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' + 'contain a valid app ID.', _a$2); var ERROR_FACTORY$1 = new ErrorFactory('analytics', 'Analytics', ERRORS); /** * @license * Copyright 2020 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. */ /** * Backoff factor for 503 errors, which we want to be conservative about * to avoid overloading servers. Each retry interval will be * BASE_INTERVAL_MILLIS * LONG_RETRY_FACTOR ^ retryCount, so the second one * will be ~30 seconds (with fuzzing). */ var LONG_RETRY_FACTOR = 30; /** * Base wait interval to multiplied by backoffFactor^backoffCount. */ var BASE_INTERVAL_MILLIS = 1000; /** * Stubbable retry data storage class. */ var RetryData = /** @class */ (function () { function RetryData(throttleMetadata, intervalMillis) { if (throttleMetadata === void 0) { throttleMetadata = {}; } if (intervalMillis === void 0) { intervalMillis = BASE_INTERVAL_MILLIS; } this.throttleMetadata = throttleMetadata; this.intervalMillis = intervalMillis; } RetryData.prototype.getThrottleMetadata = function (appId) { return this.throttleMetadata[appId]; }; RetryData.prototype.setThrottleMetadata = function (appId, metadata) { this.throttleMetadata[appId] = metadata; }; RetryData.prototype.deleteThrottleMetadata = function (appId) { delete this.throttleMetadata[appId]; }; return RetryData; }()); var defaultRetryData = new RetryData(); /** * Set GET request headers. * @param apiKey App API key. */ function getHeaders$1(apiKey) { return new Headers({ Accept: 'application/json', 'x-goog-api-key': apiKey }); } /** * Fetches dynamic config from backend. * @param app Firebase app to fetch config for. */ function fetchDynamicConfig(appFields) { var _a; return __awaiter(this, void 0, void 0, function () { var appId, apiKey, request, appUrl, response, errorMessage, jsonResponse, _ignored_1; return __generator(this, function (_b) { switch (_b.label) { case 0: appId = appFields.appId, apiKey = appFields.apiKey; request = { method: 'GET', headers: getHeaders$1(apiKey) }; appUrl = DYNAMIC_CONFIG_URL.replace('{app-id}', appId); return [4 /*yield*/, fetch(appUrl, request)]; case 1: response = _b.sent(); if (!(response.status !== 200 && response.status !== 304)) return [3 /*break*/, 6]; errorMessage = ''; _b.label = 2; case 2: _b.trys.push([2, 4, , 5]); return [4 /*yield*/, response.json()]; case 3: jsonResponse = (_b.sent()); if ((_a = jsonResponse.error) === null || _a === void 0 ? void 0 : _a.message) { errorMessage = jsonResponse.error.message; } return [3 /*break*/, 5]; case 4: _ignored_1 = _b.sent(); return [3 /*break*/, 5]; case 5: throw ERROR_FACTORY$1.create("config-fetch-failed" /* CONFIG_FETCH_FAILED */, { httpStatus: response.status, responseMessage: errorMessage }); case 6: return [2 /*return*/, response.json()]; } }); }); } /** * Fetches dynamic config from backend, retrying if failed. * @param app Firebase app to fetch config for. */ function fetchDynamicConfigWithRetry(app, // retryData and timeoutMillis are parameterized to allow passing a different value for testing. retryData, timeoutMillis) { if (retryData === void 0) { retryData = defaultRetryData; } return __awaiter(this, void 0, void 0, function () { var _a, appId, apiKey, measurementId, throttleMetadata, signal; var _this = this; return __generator(this, function (_b) { _a = app.options, appId = _a.appId, apiKey = _a.apiKey, measurementId = _a.measurementId; if (!appId) { throw ERROR_FACTORY$1.create("no-app-id" /* NO_APP_ID */); } if (!apiKey) { if (measurementId) { return [2 /*return*/, { measurementId: measurementId, appId: appId }]; } throw ERROR_FACTORY$1.create("no-api-key" /* NO_API_KEY */); } throttleMetadata = retryData.getThrottleMetadata(appId) || { backoffCount: 0, throttleEndTimeMillis: Date.now() }; signal = new AnalyticsAbortSignal(); setTimeout(function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { // Note a very low delay, eg < 10ms, can elapse before listeners are initialized. signal.abort(); return [2 /*return*/]; }); }); }, timeoutMillis !== undefined ? timeoutMillis : FETCH_TIMEOUT_MILLIS); return [2 /*return*/, attemptFetchDynamicConfigWithRetry({ appId: appId, apiKey: apiKey, measurementId: measurementId }, throttleMetadata, signal, retryData)]; }); }); } /** * Runs one retry attempt. * @param appFields Necessary app config fields. * @param throttleMetadata Ongoing metadata to determine throttling times. * @param signal Abort signal. */ function attemptFetchDynamicConfigWithRetry(appFields, _a, signal, retryData // for testing ) { var throttleEndTimeMillis = _a.throttleEndTimeMillis, backoffCount = _a.backoffCount; if (retryData === void 0) { retryData = defaultRetryData; } return __awaiter(this, void 0, void 0, function () { var appId, measurementId, e_1, response, e_2, backoffMillis, throttleMetadata; return __generator(this, function (_b) { switch (_b.label) { case 0: appId = appFields.appId, measurementId = appFields.measurementId; _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); return [4 /*yield*/, setAbortableTimeout(signal, throttleEndTimeMillis)]; case 2: _b.sent(); return [3 /*break*/, 4]; case 3: e_1 = _b.sent(); if (measurementId) { logger.warn("Timed out fetching this Firebase app's measurement ID from the server." + (" Falling back to the measurement ID " + measurementId) + (" provided in the \"measurementId\" field in the local Firebase config. [" + e_1.message + "]")); return [2 /*return*/, { appId: appId, measurementId: measurementId }]; } throw e_1; case 4: _b.trys.push([4, 6, , 7]); return [4 /*yield*/, fetchDynamicConfig(appFields)]; case 5: response = _b.sent(); // Note the SDK only clears throttle state if response is success or non-retriable. retryData.deleteThrottleMetadata(appId); return [2 /*return*/, response]; case 6: e_2 = _b.sent(); if (!isRetriableError(e_2)) { retryData.deleteThrottleMetadata(appId); if (measurementId) { logger.warn("Failed to fetch this Firebase app's measurement ID from the server." + (" Falling back to the measurement ID " + measurementId) + (" provided in the \"measurementId\" field in the local Firebase config. [" + e_2.message + "]")); return [2 /*return*/, { appId: appId, measurementId: measurementId }]; } else { throw e_2; } } backoffMillis = Number(e_2.customData.httpStatus) === 503 ? calculateBackoffMillis(backoffCount, retryData.intervalMillis, LONG_RETRY_FACTOR) : calculateBackoffMillis(backoffCount, retryData.intervalMillis); throttleMetadata = { throttleEndTimeMillis: Date.now() + backoffMillis, backoffCount: backoffCount + 1 }; // Persists state. retryData.setThrottleMetadata(appId, throttleMetadata); logger.debug("Calling attemptFetch again in " + backoffMillis + " millis"); return [2 /*return*/, attemptFetchDynamicConfigWithRetry(appFields, throttleMetadata, signal, retryData)]; case 7: return [2 /*return*/]; } }); }); } /** * Supports waiting on a backoff by: * *
Visible for testing. */ function setAbortableTimeout(signal, throttleEndTimeMillis) { return new Promise(function (resolve, reject) { // Derives backoff from given end time, normalizing negative numbers to zero. var backoffMillis = Math.max(throttleEndTimeMillis - Date.now(), 0); var timeout = setTimeout(resolve, backoffMillis); // Adds listener, rather than sets onabort, because signal is a shared object. signal.addEventListener(function () { clearTimeout(timeout); // If the request completes before this timeout, the rejection has no effect. reject(ERROR_FACTORY$1.create("fetch-throttle" /* FETCH_THROTTLE */, { throttleEndTimeMillis: throttleEndTimeMillis })); }); }); } /** * Returns true if the {@link Error} indicates a fetch request may succeed later. */ function isRetriableError(e) { if (!(e instanceof FirebaseError) || !e.customData) { return false; } // Uses string index defined by ErrorData, which FirebaseError implements. var httpStatus = Number(e.customData['httpStatus']); return (httpStatus === 429 || httpStatus === 500 || httpStatus === 503 || httpStatus === 504); } /** * Shims a minimal AbortSignal (copied from Remote Config). * *
AbortController's AbortSignal conveniently decouples fetch timeout logic from other aspects * of networking, such as retries. Firebase doesn't use AbortController enough to justify a * polyfill recommendation, like we do with the Fetch API, but this minimal shim can easily be * swapped out if/when we do. */ var AnalyticsAbortSignal = /** @class */ (function () { function AnalyticsAbortSignal() { this.listeners = []; } AnalyticsAbortSignal.prototype.addEventListener = function (listener) { this.listeners.push(listener); }; AnalyticsAbortSignal.prototype.abort = function () { this.listeners.forEach(function (listener) { return listener(); }); }; return AnalyticsAbortSignal; }()); /** * @license * Copyright 2020 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. */ function validateIndexedDB() { return __awaiter(this, void 0, void 0, function () { var e_1; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!!isIndexedDBAvailable()) return [3 /*break*/, 1]; logger.warn(ERROR_FACTORY$1.create("indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */, { errorInfo: 'IndexedDB is not available in this environment.' }).message); return [2 /*return*/, false]; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, validateIndexedDBOpenable()]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: e_1 = _a.sent(); logger.warn(ERROR_FACTORY$1.create("indexeddb-unavailable" /* INDEXEDDB_UNAVAILABLE */, { errorInfo: e_1 }).message); return [2 /*return*/, false]; case 4: return [2 /*return*/, true]; } }); }); } /** * Initialize the analytics instance in gtag.js by calling config command with fid. * * NOTE: We combine analytics initialization and setting fid together because we want fid to be * part of the `page_view` event that's sent during the initialization * @param app Firebase app * @param gtagCore The gtag function that's not wrapped. * @param dynamicConfigPromisesList Array of all dynamic config promises. * @param measurementIdToAppId Maps measurementID to appID. * @param installations _FirebaseInstallationsInternal instance. * * @returns Measurement ID. */ function initializeAnalytics(app, dynamicConfigPromisesList, measurementIdToAppId, installations, gtagCore, dataLayerName) { return __awaiter(this, void 0, void 0, function () { var dynamicConfigPromise, fidPromise, _a, dynamicConfig, fid, configProperties; var _b; return __generator(this, function (_c) { switch (_c.label) { case 0: dynamicConfigPromise = fetchDynamicConfigWithRetry(app); // Once fetched, map measurementIds to appId, for ease of lookup in wrapped gtag function. dynamicConfigPromise .then(function (config) { measurementIdToAppId[config.measurementId] = config.appId; if (app.options.measurementId && config.measurementId !== app.options.measurementId) { logger.warn("The measurement ID in the local Firebase config (" + app.options.measurementId + ")" + (" does not match the measurement ID fetched from the server (" + config.measurementId + ").") + " To ensure analytics events are always sent to the correct Analytics property," + " update the" + " measurement ID field in the local config or remove it from the local config."); } }) .catch(function (e) { return logger.error(e); }); // Add to list to track state of all dynamic config promises. dynamicConfigPromisesList.push(dynamicConfigPromise); fidPromise = validateIndexedDB().then(function (envIsValid) { if (envIsValid) { return installations.getId(); } else { return undefined; } }); return [4 /*yield*/, Promise.all([ dynamicConfigPromise, fidPromise ])]; case 1: _a = _c.sent(), dynamicConfig = _a[0], fid = _a[1]; // Detect if user has already put the gtag '; } var iframeContents = '
' + script + ''; 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); } } } /** * Each browser has its own funny way to handle iframes. Here we mush them all together into one object that I can * actually use. */ FirebaseIFrameScriptHolder.createIFrame_ = function () { var iframe = document.createElement('iframe'); iframe.style.display = 'none'; // This is necessary in order to initialize the document inside the iframe if (document.body) { document.body.appendChild(iframe); try { // If document.domain has been modified in IE, this will throw an error, and we need to set the // domain of the iframe's document manually. We can do this via a javascript: url as the src attribute // Also note that we must do this *after* the iframe has been appended to the page. Otherwise it doesn't work. var a = iframe.contentWindow.document; if (!a) { // Apologies for the log-spam, I need to do something to keep closure from optimizing out the assignment above. log('No IE domain setting required'); } } catch (e) { var domain = document.domain; iframe.src = "javascript:void((function(){document.open();document.domain='" + domain + "';document.close();})())"; } } else { // LongPollConnection attempts to delay initialization until the document is ready, so hopefully this // never gets hit. throw 'Document body has not initialized. Wait to initialize Firebase until after the document is ready.'; } // Get the document of the iframe in a browser-specific way. if (iframe.contentDocument) { iframe.doc = iframe.contentDocument; // Firefox, Opera, Safari } else if (iframe.contentWindow) { iframe.doc = iframe.contentWindow.document; // Internet Explorer // eslint-disable-next-line @typescript-eslint/no-explicit-any } else if (iframe.document) { // eslint-disable-next-line @typescript-eslint/no-explicit-any iframe.doc = iframe.document; //others? } return iframe; }; /** * Cancel all outstanding queries and remove the frame. */ FirebaseIFrameScriptHolder.prototype.close = function () { var _this = this; //Mark this iframe as dead, so no new requests are sent. this.alive = false; if (this.myIFrame) { //We have to actually remove all of the html inside this iframe before removing it from the //window, or IE will continue loading and executing the script tags we've already added, which //can lead to some errors being thrown. Setting innerHTML seems to be the easiest way to do this. this.myIFrame.doc.body.innerHTML = ''; setTimeout(function () { if (_this.myIFrame !== null) { document.body.removeChild(_this.myIFrame); _this.myIFrame = null; } }, Math.floor(0)); } // Protect from being called recursively. var onDisconnect = this.onDisconnect; if (onDisconnect) { this.onDisconnect = null; onDisconnect(); } }; /** * Actually start the long-polling session by adding the first script tag(s) to the iframe. * @param id - The ID of this connection * @param pw - The password for this connection */ FirebaseIFrameScriptHolder.prototype.startLongPoll = function (id, pw) { this.myID = id; this.myPW = pw; this.alive = true; //send the initial request. If there are requests queued, make sure that we transmit as many as we are currently able to. while (this.newRequest_()) { } }; /** * This is called any time someone might want a script tag to be added. It adds a script tag when there aren't * too many outstanding requests and we are still alive. * * If there are outstanding packet segments to send, it sends one. If there aren't, it sends a long-poll anyways if * needed. */ FirebaseIFrameScriptHolder.prototype.newRequest_ = function () { // We keep one outstanding request open all the time to receive data, but if we need to send data // (pendingSegs.length > 0) then we create a new request to send the data. The server will automatically // close the old request. if (this.alive && this.sendNewPolls && this.outstandingRequests.size < (this.pendingSegs.length > 0 ? 2 : 1)) { //construct our url this.currentSerial++; var urlParams = {}; urlParams[FIREBASE_LONGPOLL_ID_PARAM] = this.myID; urlParams[FIREBASE_LONGPOLL_PW_PARAM] = this.myPW; urlParams[FIREBASE_LONGPOLL_SERIAL_PARAM] = this.currentSerial; var theURL = this.urlFn(urlParams); //Now add as much data as we can. var curDataString = ''; var i = 0; while (this.pendingSegs.length > 0) { //first, lets see if the next segment will fit. var nextSeg = this.pendingSegs[0]; if (nextSeg.d.length + SEG_HEADER_SIZE + curDataString.length <= MAX_URL_DATA_SIZE) { //great, the segment will fit. Lets append it. var theSeg = this.pendingSegs.shift(); curDataString = curDataString + '&' + FIREBASE_LONGPOLL_SEGMENT_NUM_PARAM + i + '=' + theSeg.seg + '&' + FIREBASE_LONGPOLL_SEGMENTS_IN_PACKET + i + '=' + theSeg.ts + '&' + FIREBASE_LONGPOLL_DATA_PARAM + i + '=' + theSeg.d; i++; } else { break; } } theURL = theURL + curDataString; this.addLongPollTag_(theURL, this.currentSerial); return true; } else { return false; } }; /** * Queue a packet for transmission to the server. * @param segnum - A sequential id for this packet segment used for reassembly * @param totalsegs - The total number of segments in this packet * @param data - The data for this segment. */ FirebaseIFrameScriptHolder.prototype.enqueueSegment = function (segnum, totalsegs, data) { //add this to the queue of segments to send. this.pendingSegs.push({ seg: segnum, ts: totalsegs, d: data }); //send the data immediately if there isn't already data being transmitted, unless //startLongPoll hasn't been called yet. if (this.alive) { this.newRequest_(); } }; /** * Add a script tag for a regular long-poll request. * @param url - The URL of the script tag. * @param serial - The serial number of the request. */ FirebaseIFrameScriptHolder.prototype.addLongPollTag_ = function (url, serial) { var _this = this; //remember that we sent this request. this.outstandingRequests.add(serial); var doNewRequest = function () { _this.outstandingRequests.delete(serial); _this.newRequest_(); }; // If this request doesn't return on its own accord (by the server sending us some data), we'll // create a new one after the KEEPALIVE interval to make sure we always keep a fresh request open. var keepaliveTimeout = setTimeout(doNewRequest, Math.floor(KEEPALIVE_REQUEST_INTERVAL)); var readyStateCB = function () { // Request completed. Cancel the keepalive. clearTimeout(keepaliveTimeout); // Trigger a new request so we can continue receiving data. doNewRequest(); }; this.addTag(url, readyStateCB); }; /** * Add an arbitrary script tag to the iframe. * @param url - The URL for the script tag source. * @param loadCB - A callback to be triggered once the script has loaded. */ FirebaseIFrameScriptHolder.prototype.addTag = function (url, loadCB) { var _this = this; { setTimeout(function () { try { // if we're already closed, don't add this poll if (!_this.sendNewPolls) { return; } var newScript_1 = _this.myIFrame.doc.createElement('script'); newScript_1.type = 'text/javascript'; newScript_1.async = true; newScript_1.src = url; // eslint-disable-next-line @typescript-eslint/no-explicit-any newScript_1.onload = newScript_1.onreadystatechange = function () { // eslint-disable-next-line @typescript-eslint/no-explicit-any var rstate = newScript_1.readyState; if (!rstate || rstate === 'loaded' || rstate === 'complete') { // eslint-disable-next-line @typescript-eslint/no-explicit-any newScript_1.onload = newScript_1.onreadystatechange = null; if (newScript_1.parentNode) { newScript_1.parentNode.removeChild(newScript_1); } loadCB(); } }; newScript_1.onerror = function () { log('Long-poll script failed to load: ' + url); _this.sendNewPolls = false; _this.close(); }; _this.myIFrame.doc.body.appendChild(newScript_1); } catch (e) { // TODO: we should make this error visible somehow } }, Math.floor(1)); } }; return FirebaseIFrameScriptHolder; }()); /** * @license * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** The semver (www.semver.org) version of the SDK. */ var SDK_VERSION = ''; /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var WEBSOCKET_MAX_FRAME_SIZE = 16384; var WEBSOCKET_KEEPALIVE_INTERVAL = 45000; var WebSocketImpl = null; if (typeof MozWebSocket !== 'undefined') { WebSocketImpl = MozWebSocket; } else if (typeof WebSocket !== 'undefined') { WebSocketImpl = WebSocket; } /** * Create a new websocket connection with the given callbacks. */ var WebSocketConnection = /** @class */ (function () { /** * @param connId identifier for this transport * @param repoInfo The info for the websocket endpoint. * @param applicationId The Firebase App ID for this project. * @param transportSessionId Optional transportSessionId if this is connecting to an existing transport * session * @param lastSessionId Optional lastSessionId if there was a previous connection */ function WebSocketConnection(connId, repoInfo, applicationId, transportSessionId, lastSessionId) { this.connId = connId; this.applicationId = applicationId; this.keepaliveTimer = null; this.frames = null; this.totalFrames = 0; this.bytesSent = 0; this.bytesReceived = 0; this.log_ = logWrapper(this.connId); this.stats_ = StatsManager.getCollection(repoInfo); this.connURL = WebSocketConnection.connectionURL_(repoInfo, transportSessionId, lastSessionId); 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 * @return connection url */ WebSocketConnection.connectionURL_ = function (repoInfo, transportSessionId, lastSessionId) { var urlParams = {}; urlParams[VERSION_PARAM] = PROTOCOL_VERSION; if ( 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; } return repoInfo.connectionURL(WEBSOCKET, urlParams); }; /** * @param onMessage Callback when messages arrive * @param onDisconnect Callback with connection lost. */ WebSocketConnection.prototype.open = function (onMessage, onDisconnect) { var _this = this; this.onDisconnect = onDisconnect; this.onMessage = onMessage; this.log_('Websocket connecting to ' + this.connURL); this.everConnected_ = false; // Assume failure until proven otherwise. PersistentStorage.set('previous_websocket_failure', true); try { var device, options, env, proxy; if (isNodeSdk()) ; else { var options = { headers: { 'X-Firebase-GMPID': this.applicationId || '' } }; this.mySock = new WebSocketImpl(this.connURL, [], options); } } catch (e) { this.log_('Error instantiating WebSocket.'); var error = e.message || e.data; if (error) { this.log_(error); } this.onClosed_(); return; } this.mySock.onopen = function () { _this.log_('Websocket connected.'); _this.everConnected_ = true; }; this.mySock.onclose = function () { _this.log_('Websocket connection was disconnected.'); _this.mySock = null; _this.onClosed_(); }; this.mySock.onmessage = function (m) { _this.handleIncomingFrame(m); }; this.mySock.onerror = function (e) { _this.log_('WebSocket error. Closing connection.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any var error = e.message || e.data; if (error) { _this.log_(error); } _this.onClosed_(); }; }; /** * No-op for websockets, we don't need to do anything once the connection is confirmed as open */ WebSocketConnection.prototype.start = function () { }; WebSocketConnection.forceDisallow = function () { WebSocketConnection.forceDisallow_ = true; }; WebSocketConnection.isAvailable = function () { var isOldAndroid = false; if (typeof navigator !== 'undefined' && navigator.userAgent) { var oldAndroidRegex = /Android ([0-9]{0,}\.[0-9]{0,})/; var oldAndroidMatch = navigator.userAgent.match(oldAndroidRegex); if (oldAndroidMatch && oldAndroidMatch.length > 1) { if (parseFloat(oldAndroidMatch[1]) < 4.4) { isOldAndroid = true; } } } return (!isOldAndroid && WebSocketImpl !== null && !WebSocketConnection.forceDisallow_); }; /** * Returns true if we previously failed to connect with this transport. */ WebSocketConnection.previouslyFailed = function () { // If our persistent storage is actually only in-memory storage, // we default to assuming that it previously failed to be safe. return (PersistentStorage.isInMemoryStorage || PersistentStorage.get('previous_websocket_failure') === true); }; WebSocketConnection.prototype.markConnectionHealthy = function () { PersistentStorage.remove('previous_websocket_failure'); }; WebSocketConnection.prototype.appendFrame_ = function (data) { this.frames.push(data); if (this.frames.length === this.totalFrames) { var fullMess = this.frames.join(''); this.frames = null; var jsonMess = jsonEval(fullMess); //handle the message this.onMessage(jsonMess); } }; /** * @param frameCount The number of frames we are expecting from the server */ WebSocketConnection.prototype.handleNewFrameCount_ = function (frameCount) { this.totalFrames = frameCount; this.frames = []; }; /** * Attempts to parse a frame count out of some text. If it can't, assumes a value of 1 * @return Any remaining data to be process, or null if there is none */ WebSocketConnection.prototype.extractFrameCount_ = function (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) { var frameCount = Number(data); if (!isNaN(frameCount)) { this.handleNewFrameCount_(frameCount); return null; } } this.handleNewFrameCount_(1); return data; }; /** * Process a websocket frame that has arrived from the server. * @param mess The frame data */ WebSocketConnection.prototype.handleIncomingFrame = function (mess) { if (this.mySock === null) { return; // Chrome apparently delivers incoming packets even after we .close() the connection sometimes. } var data = mess['data']; this.bytesReceived += data.length; this.stats_.incrementCounter('bytes_received', data.length); this.resetKeepAlive(); if (this.frames !== null) { // we're buffering this.appendFrame_(data); } else { // try to parse out a frame count, otherwise, assume 1 and process it var remainingData = this.extractFrameCount_(data); if (remainingData !== null) { this.appendFrame_(remainingData); } } }; /** * Send a message to the server * @param data The JSON object to transmit */ WebSocketConnection.prototype.send = function (data) { this.resetKeepAlive(); var dataStr = stringify(data); this.bytesSent += dataStr.length; this.stats_.incrementCounter('bytes_sent', dataStr.length); //We can only fit a certain amount in each websocket frame, so we need to split this request //up into multiple pieces if it doesn't fit in one request. var dataSegs = splitStringBySize(dataStr, WEBSOCKET_MAX_FRAME_SIZE); //Send the length header if (dataSegs.length > 1) { this.sendString_(String(dataSegs.length)); } //Send the actual data in segments. for (var i = 0; i < dataSegs.length; i++) { this.sendString_(dataSegs[i]); } }; WebSocketConnection.prototype.shutdown_ = function () { this.isClosed_ = true; if (this.keepaliveTimer) { clearInterval(this.keepaliveTimer); this.keepaliveTimer = null; } if (this.mySock) { this.mySock.close(); this.mySock = null; } }; WebSocketConnection.prototype.onClosed_ = function () { if (!this.isClosed_) { this.log_('WebSocket is closing itself'); this.shutdown_(); // since this is an internal close, trigger the close listener if (this.onDisconnect) { this.onDisconnect(this.everConnected_); this.onDisconnect = null; } } }; /** * External-facing close handler. * Close the websocket and kill the connection. */ WebSocketConnection.prototype.close = function () { if (!this.isClosed_) { this.log_('WebSocket is being closed'); this.shutdown_(); } }; /** * Kill the current keepalive timer and start a new one, to ensure that it always fires N seconds after * the last activity. */ WebSocketConnection.prototype.resetKeepAlive = function () { var _this = this; clearInterval(this.keepaliveTimer); this.keepaliveTimer = setInterval(function () { //If there has been no websocket activity for a while, send a no-op if (_this.mySock) { _this.sendString_('0'); } _this.resetKeepAlive(); // eslint-disable-next-line @typescript-eslint/no-explicit-any }, Math.floor(WEBSOCKET_KEEPALIVE_INTERVAL)); }; /** * Send a string over the websocket. * * @param str String to send. */ WebSocketConnection.prototype.sendString_ = function (str) { // Firefox seems to sometimes throw exceptions (NS_ERROR_UNEXPECTED) from websocket .send() // calls for some unknown reason. We treat these as an error and disconnect. // See https://app.asana.com/0/58926111402292/68021340250410 try { this.mySock.send(str); } catch (e) { this.log_('Exception thrown from WebSocket.send():', e.message || e.data, 'Closing connection.'); setTimeout(this.onClosed_.bind(this), 0); } }; /** * Number of response before we consider the connection "healthy." */ WebSocketConnection.responsesRequiredToBeHealthy = 2; /** * Time to wait for the connection te become healthy before giving up. */ WebSocketConnection.healthyTimeout = 30000; return WebSocketConnection; }()); /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Currently simplistic, this class manages what transport a Connection should use at various stages of its * lifecycle. * * It starts with longpolling in a browser, and httppolling on node. It then upgrades to websockets if * they are available. */ var TransportManager = /** @class */ (function () { /** * @param repoInfo Metadata around the namespace we're connecting to */ function TransportManager(repoInfo) { this.initTransports_(repoInfo); } Object.defineProperty(TransportManager, "ALL_TRANSPORTS", { get: function () { return [BrowserPollConnection, WebSocketConnection]; }, enumerable: false, configurable: true }); TransportManager.prototype.initTransports_ = function (repoInfo) { var e_1, _a; var isWebSocketsAvailable = WebSocketConnection && WebSocketConnection['isAvailable'](); var isSkipPollConnection = isWebSocketsAvailable && !WebSocketConnection.previouslyFailed(); if (repoInfo.webSocketOnly) { if (!isWebSocketsAvailable) { warn("wss:// URL used, but browser isn't known to support websockets. Trying anyway."); } isSkipPollConnection = true; } if (isSkipPollConnection) { this.transports_ = [WebSocketConnection]; } else { var transports = (this.transports_ = []); try { for (var _b = __values(TransportManager.ALL_TRANSPORTS), _c = _b.next(); !_c.done; _c = _b.next()) { var transport = _c.value; if (transport && transport['isAvailable']()) { transports.push(transport); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } } }; /** * @return The constructor for the initial transport to use */ TransportManager.prototype.initialTransport = function () { if (this.transports_.length > 0) { return this.transports_[0]; } else { throw new Error('No transports available'); } }; /** * @return The constructor for the next transport, or null */ TransportManager.prototype.upgradeTransport = function () { if (this.transports_.length > 1) { return this.transports_[1]; } else { return null; } }; return TransportManager; }()); /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Abort upgrade attempt if it takes longer than 60s. var UPGRADE_TIMEOUT = 60000; // For some transports (WebSockets), we need to "validate" the transport by exchanging a few requests and responses. // If we haven't sent enough requests within 5s, we'll start sending noop ping requests. var DELAY_BEFORE_SENDING_EXTRA_REQUESTS = 5000; // If the initial data sent triggers a lot of bandwidth (i.e. it's a large put or a listen for a large amount of data) // then we may not be able to exchange our ping/pong requests within the healthy timeout. So if we reach the timeout // but we've sent/received enough bytes, we don't cancel the connection. var BYTES_SENT_HEALTHY_OVERRIDE = 10 * 1024; var BYTES_RECEIVED_HEALTHY_OVERRIDE = 100 * 1024; var MESSAGE_TYPE = 't'; var MESSAGE_DATA = 'd'; var CONTROL_SHUTDOWN = 's'; var CONTROL_RESET = 'r'; var CONTROL_ERROR = 'e'; var CONTROL_PONG = 'o'; var SWITCH_ACK = 'a'; var END_TRANSMISSION = 'n'; var PING = 'p'; var SERVER_HELLO = 'h'; /** * Creates a new real-time connection to the server using whichever method works * best in the current browser. */ var Connection = /** @class */ (function () { /** * @param id - an id for this connection * @param repoInfo_ - the info for the endpoint to connect to * @param applicationId_ - the Firebase App ID for this project * @param onMessage_ - the callback to be triggered when a server-push message arrives * @param onReady_ - the callback to be triggered when this connection is ready to send messages. * @param onDisconnect_ - the callback to be triggered when a connection was lost * @param onKill_ - the callback to be triggered when this connection has permanently shut down. * @param lastSessionId - last session id in persistent connection. is used to clean up old session in real-time server */ function Connection(id, repoInfo_, applicationId_, onMessage_, onReady_, onDisconnect_, onKill_, lastSessionId) { this.id = id; this.repoInfo_ = repoInfo_; this.applicationId_ = applicationId_; this.onMessage_ = onMessage_; this.onReady_ = onReady_; this.onDisconnect_ = onDisconnect_; this.onKill_ = onKill_; this.lastSessionId = lastSessionId; this.connectionCount = 0; this.pendingDataMessages = []; this.state_ = 0 /* CONNECTING */; this.log_ = logWrapper('c:' + this.id + ':'); this.transportManager_ = new TransportManager(repoInfo_); this.log_('Connection created'); this.start_(); } /** * Starts a connection attempt */ Connection.prototype.start_ = function () { var _this = this; var conn = this.transportManager_.initialTransport(); this.conn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, undefined, this.lastSessionId); // For certain transports (WebSockets), we need to send and receive several messages back and forth before we // can consider the transport healthy. this.primaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0; var onMessageReceived = this.connReceiver_(this.conn_); var onConnectionLost = this.disconnReceiver_(this.conn_); this.tx_ = this.conn_; this.rx_ = this.conn_; this.secondaryConn_ = null; this.isHealthy_ = false; /* * Firefox doesn't like when code from one iframe tries to create another iframe by way of the parent frame. * This can occur in the case of a redirect, i.e. we guessed wrong on what server to connect to and received a reset. * Somehow, setTimeout seems to make this ok. That doesn't make sense from a security perspective, since you should * still have the context of your originating frame. */ setTimeout(function () { // this.conn_ gets set to null in some of the tests. Check to make sure it still exists before using it _this.conn_ && _this.conn_.open(onMessageReceived, onConnectionLost); }, Math.floor(0)); var healthyTimeoutMS = conn['healthyTimeout'] || 0; if (healthyTimeoutMS > 0) { this.healthyTimeout_ = setTimeoutNonBlocking(function () { _this.healthyTimeout_ = null; if (!_this.isHealthy_) { if (_this.conn_ && _this.conn_.bytesReceived > BYTES_RECEIVED_HEALTHY_OVERRIDE) { _this.log_('Connection exceeded healthy timeout but has received ' + _this.conn_.bytesReceived + ' bytes. Marking connection healthy.'); _this.isHealthy_ = true; _this.conn_.markConnectionHealthy(); } else if (_this.conn_ && _this.conn_.bytesSent > BYTES_SENT_HEALTHY_OVERRIDE) { _this.log_('Connection exceeded healthy timeout but has sent ' + _this.conn_.bytesSent + ' bytes. Leaving connection alive.'); // NOTE: We don't want to mark it healthy, since we have no guarantee that the bytes have made it to // the server. } else { _this.log_('Closing unhealthy connection after timeout.'); _this.close(); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any }, Math.floor(healthyTimeoutMS)); } }; Connection.prototype.nextTransportId_ = function () { return 'c:' + this.id + ':' + this.connectionCount++; }; Connection.prototype.disconnReceiver_ = function (conn) { var _this = this; return function (everConnected) { if (conn === _this.conn_) { _this.onConnectionLost_(everConnected); } else if (conn === _this.secondaryConn_) { _this.log_('Secondary connection lost.'); _this.onSecondaryConnectionLost_(); } else { _this.log_('closing an old connection'); } }; }; Connection.prototype.connReceiver_ = function (conn) { var _this = this; return function (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 */ Connection.prototype.sendRequest = function (dataMsg) { // wrap in a data message envelope and send it on var msg = { t: 'd', d: dataMsg }; this.sendData_(msg); }; Connection.prototype.tryCleanupConnection = function () { 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 } }; Connection.prototype.onSecondaryControl_ = function (controlData) { if (MESSAGE_TYPE in controlData) { var 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_(); } } }; Connection.prototype.onSecondaryMessageReceived_ = function (parsedData) { var layer = requireKey('t', parsedData); var 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); } }; Connection.prototype.upgradeIfSecondaryHealthy_ = function () { 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: {} } }); } }; Connection.prototype.proceedWithUpgrade_ = function () { // 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(); }; Connection.prototype.onPrimaryMessageReceived_ = function (parsedData) { // Must refer to parsedData properties in quotes, so closure doesn't touch them. var layer = requireKey('t', parsedData); var data = requireKey('d', parsedData); if (layer === 'c') { this.onControl_(data); } else if (layer === 'd') { this.onDataMessage_(data); } }; Connection.prototype.onDataMessage_ = function (message) { this.onPrimaryResponse_(); // We don't do anything with data messages, just kick them up a level this.onMessage_(message); }; Connection.prototype.onPrimaryResponse_ = function () { if (!this.isHealthy_) { this.primaryResponsesRequired_--; if (this.primaryResponsesRequired_ <= 0) { this.log_('Primary connection is healthy.'); this.isHealthy_ = true; this.conn_.markConnectionHealthy(); } } }; Connection.prototype.onControl_ = function (controlData) { var cmd = requireKey(MESSAGE_TYPE, controlData); if (MESSAGE_DATA in controlData) { var payload = controlData[MESSAGE_DATA]; if (cmd === SERVER_HELLO) { this.onHandshake_(payload); } else if (cmd === END_TRANSMISSION) { this.log_('recvd end transmission on primary'); this.rx_ = this.secondaryConn_; for (var i = 0; i < this.pendingDataMessages.length; ++i) { this.onDataMessage_(this.pendingDataMessages[i]); } this.pendingDataMessages = []; this.tryCleanupConnection(); } else if (cmd === CONTROL_SHUTDOWN) { // This was previously the 'onKill' callback passed to the lower-level connection // payload in this case is the reason for the shutdown. Generally a human-readable error this.onConnectionShutdown_(payload); } else if (cmd === CONTROL_RESET) { // payload in this case is the host we should contact this.onReset_(payload); } else if (cmd === CONTROL_ERROR) { error('Server Error: ' + payload); } else if (cmd === CONTROL_PONG) { this.log_('got pong on primary.'); this.onPrimaryResponse_(); this.sendPingOnPrimaryIfNecessary_(); } else { error('Unknown control packet command: ' + cmd); } } }; /** * @param handshake The handshake data returned from the server */ Connection.prototype.onHandshake_ = function (handshake) { var timestamp = handshake.ts; var version = handshake.v; var host = handshake.h; this.sessionId = handshake.s; this.repoInfo_.updateHost(host); // if we've already closed the connection, then don't bother trying to progress further if (this.state_ === 0 /* CONNECTING */) { this.conn_.start(); this.onConnectionEstablished_(this.conn_, timestamp); if (PROTOCOL_VERSION !== version) { warn('Protocol version mismatch detected'); } // TODO: do we want to upgrade? when? maybe a delay? this.tryStartUpgrade_(); } }; Connection.prototype.tryStartUpgrade_ = function () { var conn = this.transportManager_.upgradeTransport(); if (conn) { this.startUpgrade_(conn); } }; Connection.prototype.startUpgrade_ = function (conn) { var _this = this; this.secondaryConn_ = new conn(this.nextTransportId_(), this.repoInfo_, this.applicationId_, this.sessionId); // For certain transports (WebSockets), we need to send and receive several messages back and forth before we // can consider the transport healthy. this.secondaryResponsesRequired_ = conn['responsesRequiredToBeHealthy'] || 0; var onMessage = this.connReceiver_(this.secondaryConn_); var onDisconnect = this.disconnReceiver_(this.secondaryConn_); this.secondaryConn_.open(onMessage, onDisconnect); // If we haven't successfully upgraded after UPGRADE_TIMEOUT, give up and kill the secondary. setTimeoutNonBlocking(function () { if (_this.secondaryConn_) { _this.log_('Timed out trying to upgrade.'); _this.secondaryConn_.close(); } }, Math.floor(UPGRADE_TIMEOUT)); }; Connection.prototype.onReset_ = function (host) { this.log_('Reset packet received. New host: ' + host); this.repoInfo_.updateHost(host); // TODO: if we're already "connected", we need to trigger a disconnect at the next layer up. // We don't currently support resets after the connection has already been established if (this.state_ === 1 /* CONNECTED */) { this.close(); } else { // Close whatever connections we have open and start again. this.closeConnections_(); this.start_(); } }; Connection.prototype.onConnectionEstablished_ = function (conn, timestamp) { var _this = this; this.log_('Realtime connection established.'); this.conn_ = conn; this.state_ = 1 /* CONNECTED */; if (this.onReady_) { this.onReady_(timestamp, this.sessionId); this.onReady_ = null; } // If after 5 seconds we haven't sent enough requests to the server to get the connection healthy, // send some pings. if (this.primaryResponsesRequired_ === 0) { this.log_('Primary connection is healthy.'); this.isHealthy_ = true; } else { setTimeoutNonBlocking(function () { _this.sendPingOnPrimaryIfNecessary_(); }, Math.floor(DELAY_BEFORE_SENDING_EXTRA_REQUESTS)); } }; Connection.prototype.sendPingOnPrimaryIfNecessary_ = function () { // If the connection isn't considered healthy yet, we'll send a noop ping packet request. if (!this.isHealthy_ && this.state_ === 1 /* CONNECTED */) { this.log_('sending ping on primary.'); this.sendData_({ t: 'c', d: { t: PING, d: {} } }); } }; Connection.prototype.onSecondaryConnectionLost_ = function () { var conn = this.secondaryConn_; this.secondaryConn_ = null; if (this.tx_ === conn || this.rx_ === conn) { // we are relying on this connection already in some capacity. Therefore, a failure is real this.close(); } }; /** * @param everConnected Whether or not the connection ever reached a server. Used to determine if * we should flush the host cache */ Connection.prototype.onConnectionLost_ = function (everConnected) { this.conn_ = null; // NOTE: IF you're seeing a Firefox error for this line, I think it might be because it's getting // called on window close and RealtimeState.CONNECTING is no longer defined. Just a guess. if (!everConnected && this.state_ === 0 /* CONNECTING */) { this.log_('Realtime connection failed.'); // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away if (this.repoInfo_.isCacheableHost()) { PersistentStorage.remove('host:' + this.repoInfo_.host); // reset the internal host to what we would show the user, i.e.Note: This method must be called before performing any other operation. * * @param host the emulator host (ex: localhost) * @param port the emulator port (ex: 8080) */ Database.prototype.useEmulator = function (host, port) { this.checkDeleted_('useEmulator'); if (this.instanceStarted_) { fatal('Cannot call useEmulator() after instance has already been initialized.'); return; } // Modify the repo to apply emulator settings RepoManager.getInstance().applyEmulatorSettings(this.repoInternal_, host, port); }; Database.prototype.ref = function (path) { this.checkDeleted_('ref'); validateArgCount('database.ref', 0, 1, arguments.length); if (path instanceof Reference) { return this.refFromURL(path.toString()); } return path !== undefined ? this.root_.child(path) : this.root_; }; /** * Returns a reference to the root or the path specified in url. * We throw a exception if the url is not in the same domain as the * current repo. * @return Firebase reference. */ Database.prototype.refFromURL = function (url) { /** @const {string} */ var apiName = 'database.refFromURL'; this.checkDeleted_(apiName); validateArgCount(apiName, 1, 1, arguments.length); var parsedURL = parseRepoInfo(url, this.repo_.repoInfo_.nodeAdmin); validateUrl(apiName, 1, parsedURL); var repoInfo = parsedURL.repoInfo; if (!this.repo_.repoInfo_.isCustomHost() && repoInfo.host !== this.repo_.repoInfo_.host) { fatal(apiName + ': Host name does not match the current database: ' + '(found ' + repoInfo.host + ' but expected ' + this.repo_.repoInfo_.host + ')'); } return this.ref(parsedURL.path.toString()); }; Database.prototype.checkDeleted_ = function (apiName) { if (this.repoInternal_ === null) { fatal('Cannot call ' + apiName + ' on a deleted database.'); } }; // Make individual repo go offline. Database.prototype.goOffline = function () { validateArgCount('database.goOffline', 0, 0, arguments.length); this.checkDeleted_('goOffline'); repoInterrupt(this.repo_); }; Database.prototype.goOnline = function () { validateArgCount('database.goOnline', 0, 0, arguments.length); this.checkDeleted_('goOnline'); repoResume(this.repo_); }; Database.ServerValue = { TIMESTAMP: { '.sv': 'timestamp' }, increment: function (delta) { return { '.sv': { 'increment': delta } }; } }; return Database; }()); /** * @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. */ /** * Node in a Tree. */ var TreeNode = /** @class */ (function () { function TreeNode() { // TODO: Consider making accessors that create children and value lazily or // separate Internal / Leaf 'types'. this.children = {}; this.childCount = 0; this.value = null; } return TreeNode; }()); /** * A light-weight tree, traversable by path. Nodes can have both values and children. * Nodes are not enumerated (by forEachChild) unless they have a value or non-empty * children. */ var Tree = /** @class */ (function () { /** * @param name_ Optional name of the node. * @param parent_ Optional parent node. * @param node_ Optional node to wrap. */ function Tree(name_, parent_, node_) { if (name_ === void 0) { name_ = ''; } if (parent_ === void 0) { parent_ = null; } if (node_ === void 0) { node_ = new TreeNode(); } this.name_ = name_; this.parent_ = parent_; this.node_ = node_; } /** * Returns a sub-Tree for the given path. * * @param pathObj Path to look up. * @return Tree for path. */ Tree.prototype.subTree = function (pathObj) { // TODO: Require pathObj to be Path? var path = pathObj instanceof Path ? pathObj : new Path(pathObj); var child = this, next = pathGetFront(path); while (next !== null) { var childNode = safeGet(child.node_.children, next) || new TreeNode(); child = new Tree(next, child, childNode); path = pathPopFront(path); next = pathGetFront(path); } return child; }; /** * Returns the data associated with this tree node. * * @return The data or null if no data exists. */ Tree.prototype.getValue = function () { return this.node_.value; }; /** * Sets data to this tree node. * * @param value Value to set. */ Tree.prototype.setValue = function (value) { assert(typeof value !== 'undefined', 'Cannot set value to undefined'); this.node_.value = value; this.updateParents_(); }; /** * Clears the contents of the tree node (its value and all children). */ Tree.prototype.clear = function () { this.node_.value = null; this.node_.children = {}; this.node_.childCount = 0; this.updateParents_(); }; /** * @return Whether the tree has any children. */ Tree.prototype.hasChildren = function () { return this.node_.childCount > 0; }; /** * @return Whethe rthe tree is empty (no value or children). */ Tree.prototype.isEmpty = function () { return this.getValue() === null && !this.hasChildren(); }; /** * Calls action for each child of this tree node. * * @param action Action to be called for each child. */ Tree.prototype.forEachChild = function (action) { var _this = this; each(this.node_.children, function (child, childTree) { action(new Tree(child, _this, childTree)); }); }; /** * Does a depth-first traversal of this node's descendants, calling action for each one. * * @param action Action to be called for each child. * @param includeSelf Whether to call action on this node as well. Defaults to * false. * @param childrenFirst Whether to call action on children before calling it on * parent. */ Tree.prototype.forEachDescendant = function (action, includeSelf, childrenFirst) { if (includeSelf && !childrenFirst) { action(this); } this.forEachChild(function (child) { child.forEachDescendant(action, /*includeSelf=*/ true, childrenFirst); }); if (includeSelf && childrenFirst) { action(this); } }; /** * Calls action on each ancestor node. * * @param action Action to be called on each parent; return * true to abort. * @param includeSelf Whether to call action on this node as well. * @return true if the action callback returned true. */ Tree.prototype.forEachAncestor = function (action, includeSelf) { var node = includeSelf ? this : this.parent(); while (node !== null) { if (action(node)) { return true; } node = node.parent(); } return false; }; /** * Does a depth-first traversal of this node's descendants. When a descendant with a value * is found, action is called on it and traversal does not continue inside the node. * Action is *not* called on this node. * * @param action Action to be called for each child. */ Tree.prototype.forEachImmediateDescendantWithValue = function (action) { this.forEachChild(function (child) { if (child.getValue() !== null) { action(child); } else { child.forEachImmediateDescendantWithValue(action); } }); }; /** * @return The path of this tree node, as a Path. */ Tree.prototype.path = function () { return new Path(this.parent_ === null ? this.name_ : this.parent_.path() + '/' + this.name_); }; /** * @return The name of the tree node. */ Tree.prototype.name = function () { return this.name_; }; /** * @return The parent tree node, or null if this is the root of the tree. */ Tree.prototype.parent = function () { return this.parent_; }; /** * Adds or removes this child from its parent based on whether it's empty or not. */ Tree.prototype.updateParents_ = function () { if (this.parent_ !== null) { this.parent_.updateChild_(this.name_, this); } }; /** * Adds or removes the passed child to this tree node, depending on whether it's empty. * * @param childName The name of the child to update. * @param child The child to update. */ Tree.prototype.updateChild_ = function (childName, child) { var childEmpty = child.isEmpty(); var childExists = contains(this.node_.children, childName); if (childEmpty && childExists) { delete this.node_.children[childName]; this.node_.childCount--; this.updateParents_(); } else if (!childEmpty && !childExists) { this.node_.children[childName] = child.node_; this.node_.childCount++; this.updateParents_(); } }; return Tree; }()); /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var INTERRUPT_REASON = 'repo_interrupt'; /** * If a transaction does not succeed after 25 retries, we abort it. Among other * things this ensure that if there's ever a bug causing a mismatch between * client / server hashes for some data, we won't retry indefinitely. */ var MAX_TRANSACTION_RETRIES = 25; /** * A connection to a single data repository. */ var Repo = /** @class */ (function () { function Repo(repoInfo_, forceRestClient_, app, authTokenProvider_) { this.repoInfo_ = repoInfo_; this.forceRestClient_ = forceRestClient_; this.app = app; this.authTokenProvider_ = authTokenProvider_; this.dataUpdateCount = 0; this.statsListener_ = null; this.eventQueue_ = new EventQueue(); this.nextWriteId_ = 1; this.interceptServerDataCallback_ = null; /** A list of data pieces and paths to be set when this client disconnects. */ this.onDisconnect_ = new SparseSnapshotTree(); /** Stores queues of outstanding transactions for Firebase locations. */ this.transactionQueueTree_ = new Tree(); // TODO: This should be @private but it's used by test_access.js and internal.js this.persistentConnection_ = null; // This key is intentionally not updated if RepoInfo is later changed or replaced this.key = this.repoInfo_.toURLString(); } /** * @return The URL corresponding to the root of this Firebase. */ Repo.prototype.toString = function () { return ((this.repoInfo_.secure ? 'https://' : 'http://') + this.repoInfo_.host); }; return Repo; }()); function repoStart(repo) { repo.stats_ = StatsManager.getCollection(repo.repoInfo_); if (repo.forceRestClient_ || beingCrawled()) { repo.server_ = new ReadonlyRestClient(repo.repoInfo_, function (pathString, data, isMerge, tag) { repoOnDataUpdate(repo, pathString, data, isMerge, tag); }, repo.authTokenProvider_); // Minor hack: Fire onConnect immediately, since there's no actual connection. setTimeout(function () { return repoOnConnectStatus(repo, /* connectStatus= */ true); }, 0); } else { var authOverride = repo.app.options['databaseAuthVariableOverride']; // Validate authOverride if (typeof authOverride !== 'undefined' && authOverride !== null) { if (typeof authOverride !== 'object') { throw new Error('Only objects are supported for option databaseAuthVariableOverride'); } try { stringify(authOverride); } catch (e) { throw new Error('Invalid authOverride provided: ' + e); } } repo.persistentConnection_ = new PersistentConnection(repo.repoInfo_, repo.app.options.appId, function (pathString, data, isMerge, tag) { repoOnDataUpdate(repo, pathString, data, isMerge, tag); }, function (connectStatus) { repoOnConnectStatus(repo, connectStatus); }, function (updates) { repoOnServerInfoUpdate(repo, updates); }, repo.authTokenProvider_, authOverride); repo.server_ = repo.persistentConnection_; } repo.authTokenProvider_.addTokenChangeListener(function (token) { repo.server_.refreshAuthToken(token); }); // In the case of multiple Repos for the same repoInfo (i.e. there are multiple Firebase.Contexts being used), // we only want to create one StatsReporter. As such, we'll report stats over the first Repo created. repo.statsReporter_ = StatsManager.getOrCreateReporter(repo.repoInfo_, function () { return new StatsReporter(repo.stats_, repo.server_); }); // Used for .info. repo.infoData_ = new SnapshotHolder(); repo.infoSyncTree_ = new SyncTree({ startListening: function (query, tag, currentHashFn, onComplete) { var infoEvents = []; var node = repo.infoData_.getNode(query.path); // This is possibly a hack, but we have different semantics for .info endpoints. We don't raise null events // on initial data... if (!node.isEmpty()) { infoEvents = repo.infoSyncTree_.applyServerOverwrite(query.path, node); setTimeout(function () { onComplete('ok'); }, 0); } return infoEvents; }, stopListening: function () { } }); repoUpdateInfo(repo, 'connected', false); repo.serverSyncTree_ = new SyncTree({ startListening: function (query, tag, currentHashFn, onComplete) { repo.server_.listen(query, currentHashFn, tag, function (status, data) { var events = onComplete(status, data); eventQueueRaiseEventsForChangedPath(repo.eventQueue_, query.path, events); }); // No synchronous events for network-backed sync trees return []; }, stopListening: function (query, tag) { repo.server_.unlisten(query, tag); } }); } /** * @return The time in milliseconds, taking the server offset into account if we have one. */ function repoServerTime(repo) { var offsetNode = repo.infoData_.getNode(new Path('.info/serverTimeOffset')); var offset = offsetNode.val() || 0; return new Date().getTime() + offset; } /** * Generate ServerValues using some variables from the repo object. */ function repoGenerateServerValues(repo) { return generateWithValues({ timestamp: repoServerTime(repo) }); } /** * Called by realtime when we get new messages from the server. */ function repoOnDataUpdate(repo, pathString, data, isMerge, tag) { // For testing. repo.dataUpdateCount++; var path = new Path(pathString); data = repo.interceptServerDataCallback_ ? repo.interceptServerDataCallback_(pathString, data) : data; var events = []; if (tag) { if (isMerge) { var taggedChildren = map(data, function (raw) { return nodeFromJSON$1(raw); }); events = repo.serverSyncTree_.applyTaggedQueryMerge(path, taggedChildren, tag); } else { var taggedSnap = nodeFromJSON$1(data); events = repo.serverSyncTree_.applyTaggedQueryOverwrite(path, taggedSnap, tag); } } else if (isMerge) { var changedChildren = map(data, function (raw) { return nodeFromJSON$1(raw); }); events = repo.serverSyncTree_.applyServerMerge(path, changedChildren); } else { var snap = nodeFromJSON$1(data); events = repo.serverSyncTree_.applyServerOverwrite(path, snap); } var affectedPath = path; if (events.length > 0) { // Since we have a listener outstanding for each transaction, receiving any events // is a proxy for some change having occurred. affectedPath = repoRerunTransactions(repo, path); } eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, events); } function repoOnConnectStatus(repo, connectStatus) { repoUpdateInfo(repo, 'connected', connectStatus); if (connectStatus === false) { repoRunOnDisconnectEvents(repo); } } function repoOnServerInfoUpdate(repo, updates) { each(updates, function (key, value) { repoUpdateInfo(repo, key, value); }); } function repoUpdateInfo(repo, pathString, value) { var path = new Path('/.info/' + pathString); var newNode = nodeFromJSON$1(value); repo.infoData_.updateSnapshot(path, newNode); var events = repo.infoSyncTree_.applyServerOverwrite(path, newNode); eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events); } function repoGetNextWriteId(repo) { return repo.nextWriteId_++; } /** * The purpose of `getValue` is to return the latest known value * satisfying `query`. * * This method will first check for in-memory cached values * belonging to active listeners. If they are found, such values * are considered to be the most up-to-date. * * If the client is not connected, this method will try to * establish a connection and request the value for `query`. If * the client is not able to retrieve the query result, it reports * an error. * * @param query - The query to surface a value for. */ function repoGetValue(repo, query) { // Only active queries are cached. There is no persisted cache. var cached = repo.serverSyncTree_.getServerValue(query); if (cached != null) { return Promise.resolve(new DataSnapshot(cached, query.getRef(), query.getQueryParams().getIndex())); } return repo.server_.get(query).then(function (payload) { var node = nodeFromJSON$1(payload); var events = repo.serverSyncTree_.applyServerOverwrite(query.path, node); eventQueueRaiseEventsAtPath(repo.eventQueue_, query.path, events); return Promise.resolve(new DataSnapshot(node, query.getRef(), query.getQueryParams().getIndex())); }, function (err) { repoLog(repo, 'get for query ' + stringify(query) + ' failed: ' + err); return Promise.reject(new Error(err)); }); } function repoSetWithPriority(repo, path, newVal, newPriority, onComplete) { repoLog(repo, 'set', { path: path.toString(), value: newVal, priority: newPriority }); // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or // (b) store unresolved paths on JSON parse var serverValues = repoGenerateServerValues(repo); var newNodeUnresolved = nodeFromJSON$1(newVal, newPriority); var existing = repo.serverSyncTree_.calcCompleteEventCache(path); var newNode = resolveDeferredValueSnapshot(newNodeUnresolved, existing, serverValues); var writeId = repoGetNextWriteId(repo); var events = repo.serverSyncTree_.applyUserOverwrite(path, newNode, writeId, true); eventQueueQueueEvents(repo.eventQueue_, events); repo.server_.put(path.toString(), newNodeUnresolved.val(/*export=*/ true), function (status, errorReason) { var success = status === 'ok'; if (!success) { warn('set at ' + path + ' failed: ' + status); } var clearEvents = repo.serverSyncTree_.ackUserWrite(writeId, !success); eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, clearEvents); repoCallOnCompleteCallback(repo, onComplete, status, errorReason); }); var affectedPath = repoAbortTransactions(repo, path); repoRerunTransactions(repo, affectedPath); // We queued the events above, so just flush the queue here eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, []); } function repoUpdate(repo, path, childrenToMerge, onComplete) { repoLog(repo, 'update', { path: path.toString(), value: childrenToMerge }); // Start with our existing data and merge each child into it. var empty = true; var serverValues = repoGenerateServerValues(repo); var changedChildren = {}; each(childrenToMerge, function (changedKey, changedValue) { empty = false; changedChildren[changedKey] = resolveDeferredValueTree(pathChild(path, changedKey), nodeFromJSON$1(changedValue), repo.serverSyncTree_, serverValues); }); if (!empty) { var writeId_1 = repoGetNextWriteId(repo); var events = repo.serverSyncTree_.applyUserMerge(path, changedChildren, writeId_1); eventQueueQueueEvents(repo.eventQueue_, events); repo.server_.merge(path.toString(), childrenToMerge, function (status, errorReason) { var success = status === 'ok'; if (!success) { warn('update at ' + path + ' failed: ' + status); } var clearEvents = repo.serverSyncTree_.ackUserWrite(writeId_1, !success); var affectedPath = clearEvents.length > 0 ? repoRerunTransactions(repo, path) : path; eventQueueRaiseEventsForChangedPath(repo.eventQueue_, affectedPath, clearEvents); repoCallOnCompleteCallback(repo, onComplete, status, errorReason); }); each(childrenToMerge, function (changedPath) { var affectedPath = repoAbortTransactions(repo, pathChild(path, changedPath)); repoRerunTransactions(repo, affectedPath); }); // We queued the events above, so just flush the queue here eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, []); } else { log("update() called with empty data. Don't do anything."); repoCallOnCompleteCallback(repo, onComplete, 'ok', undefined); } } /** * Applies all of the changes stored up in the onDisconnect_ tree. */ function repoRunOnDisconnectEvents(repo) { repoLog(repo, 'onDisconnectEvents'); var serverValues = repoGenerateServerValues(repo); var resolvedOnDisconnectTree = new SparseSnapshotTree(); repo.onDisconnect_.forEachTree(newEmptyPath(), function (path, node) { var resolved = resolveDeferredValueTree(path, node, repo.serverSyncTree_, serverValues); resolvedOnDisconnectTree.remember(path, resolved); }); var events = []; resolvedOnDisconnectTree.forEachTree(newEmptyPath(), function (path, snap) { events = events.concat(repo.serverSyncTree_.applyServerOverwrite(path, snap)); var affectedPath = repoAbortTransactions(repo, path); repoRerunTransactions(repo, affectedPath); }); repo.onDisconnect_ = new SparseSnapshotTree(); eventQueueRaiseEventsForChangedPath(repo.eventQueue_, newEmptyPath(), events); } function repoOnDisconnectCancel(repo, path, onComplete) { repo.server_.onDisconnectCancel(path.toString(), function (status, errorReason) { if (status === 'ok') { repo.onDisconnect_.forget(path); } repoCallOnCompleteCallback(repo, onComplete, status, errorReason); }); } function repoOnDisconnectSet(repo, path, value, onComplete) { var newNode = nodeFromJSON$1(value); repo.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), function (status, errorReason) { if (status === 'ok') { repo.onDisconnect_.remember(path, newNode); } repoCallOnCompleteCallback(repo, onComplete, status, errorReason); }); } function repoOnDisconnectSetWithPriority(repo, path, value, priority, onComplete) { var newNode = nodeFromJSON$1(value, priority); repo.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), function (status, errorReason) { if (status === 'ok') { repo.onDisconnect_.remember(path, newNode); } repoCallOnCompleteCallback(repo, onComplete, status, errorReason); }); } function repoOnDisconnectUpdate(repo, path, childrenToMerge, onComplete) { if (isEmpty(childrenToMerge)) { log("onDisconnect().update() called with empty data. Don't do anything."); repoCallOnCompleteCallback(repo, onComplete, 'ok', undefined); return; } repo.server_.onDisconnectMerge(path.toString(), childrenToMerge, function (status, errorReason) { if (status === 'ok') { each(childrenToMerge, function (childName, childNode) { var newChildNode = nodeFromJSON$1(childNode); repo.onDisconnect_.remember(pathChild(path, childName), newChildNode); }); } repoCallOnCompleteCallback(repo, onComplete, status, errorReason); }); } function repoAddEventCallbackForQuery(repo, query, eventRegistration) { var events; if (pathGetFront(query.path) === '.info') { events = repo.infoSyncTree_.addEventRegistration(query, eventRegistration); } else { events = repo.serverSyncTree_.addEventRegistration(query, eventRegistration); } eventQueueRaiseEventsAtPath(repo.eventQueue_, query.path, events); } function repoRemoveEventCallbackForQuery(repo, query, eventRegistration) { // These are guaranteed not to raise events, since we're not passing in a cancelError. However, we can future-proof // a little bit by handling the return values anyways. var events; if (pathGetFront(query.path) === '.info') { events = repo.infoSyncTree_.removeEventRegistration(query, eventRegistration); } else { events = repo.serverSyncTree_.removeEventRegistration(query, eventRegistration); } eventQueueRaiseEventsAtPath(repo.eventQueue_, query.path, events); } function repoInterrupt(repo) { if (repo.persistentConnection_) { repo.persistentConnection_.interrupt(INTERRUPT_REASON); } } function repoResume(repo) { if (repo.persistentConnection_) { repo.persistentConnection_.resume(INTERRUPT_REASON); } } function repoLog(repo) { var varArgs = []; for (var _i = 1; _i < arguments.length; _i++) { varArgs[_i - 1] = arguments[_i]; } var prefix = ''; if (repo.persistentConnection_) { prefix = repo.persistentConnection_.id + ':'; } log.apply(void 0, __spread([prefix], varArgs)); } function repoCallOnCompleteCallback(repo, callback, status, errorReason) { if (callback) { exceptionGuard(function () { if (status === 'ok') { callback(null); } else { var code = (status || 'error').toUpperCase(); var message = code; if (errorReason) { message += ': ' + errorReason; } var error = new Error(message); // eslint-disable-next-line @typescript-eslint/no-explicit-any error.code = code; callback(error); } }); } } function repoGetDatabase(repo) { return repo.__database || (repo.__database = new Database(repo)); } /** * Creates a new transaction, adds it to the transactions we're tracking, and * sends it to the server if possible. * * @param path Path at which to do transaction. * @param transactionUpdate Update callback. * @param onComplete Completion callback. * @param applyLocally Whether or not to make intermediate results visible */ function repoStartTransaction(repo, path, transactionUpdate, onComplete, applyLocally) { repoLog(repo, 'transaction on ' + path); // Add a watch to make sure we get server updates. var valueCallback = function () { }; var watchRef = new Reference(repo, path); watchRef.on('value', valueCallback); var unwatcher = function () { watchRef.off('value', valueCallback); }; // Initialize transaction. var transaction = { path: path, update: transactionUpdate, onComplete: onComplete, // One of TransactionStatus enums. status: null, // Used when combining transactions at different locations to figure out // which one goes first. order: LUIDGenerator(), // Whether to raise local events for this transaction. applyLocally: applyLocally, // Count of how many times we've retried the transaction. retryCount: 0, // Function to call to clean up our .on() listener. unwatcher: unwatcher, // Stores why a transaction was aborted. abortReason: null, currentWriteId: null, currentInputSnapshot: null, currentOutputSnapshotRaw: null, currentOutputSnapshotResolved: null }; // Run transaction initially. var currentState = repoGetLatestState(repo, path, undefined); transaction.currentInputSnapshot = currentState; var newVal = transaction.update(currentState.val()); if (newVal === undefined) { // Abort transaction. transaction.unwatcher(); transaction.currentOutputSnapshotRaw = null; transaction.currentOutputSnapshotResolved = null; if (transaction.onComplete) { // We just set the input snapshot, so this cast should be safe var snapshot = new DataSnapshot(transaction.currentInputSnapshot, new Reference(repo, transaction.path), PRIORITY_INDEX); transaction.onComplete(null, false, snapshot); } } else { validateFirebaseData('transaction failed: Data returned ', newVal, transaction.path); // Mark as run and add to our queue. transaction.status = 0 /* RUN */; var queueNode = repo.transactionQueueTree_.subTree(path); var nodeQueue = queueNode.getValue() || []; nodeQueue.push(transaction); queueNode.setValue(nodeQueue); // Update visibleData and raise events // Note: We intentionally raise events after updating all of our // transaction state, since the user could start new transactions from the // event callbacks. var priorityForNode = void 0; if (typeof newVal === 'object' && newVal !== null && contains(newVal, '.priority')) { // eslint-disable-next-line @typescript-eslint/no-explicit-any priorityForNode = safeGet(newVal, '.priority'); assert(isValidPriority(priorityForNode), 'Invalid priority returned by transaction. ' + 'Priority must be a valid string, finite number, server value, or null.'); } else { var currentNode = repo.serverSyncTree_.calcCompleteEventCache(path) || ChildrenNode.EMPTY_NODE; priorityForNode = currentNode.getPriority().val(); } var serverValues = repoGenerateServerValues(repo); var newNodeUnresolved = nodeFromJSON$1(newVal, priorityForNode); var newNode = resolveDeferredValueSnapshot(newNodeUnresolved, currentState, serverValues); transaction.currentOutputSnapshotRaw = newNodeUnresolved; transaction.currentOutputSnapshotResolved = newNode; transaction.currentWriteId = repoGetNextWriteId(repo); var events = repo.serverSyncTree_.applyUserOverwrite(path, newNode, transaction.currentWriteId, transaction.applyLocally); eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events); repoSendReadyTransactions(repo, repo.transactionQueueTree_); } } /** * @param excludeSets A specific set to exclude */ function repoGetLatestState(repo, path, excludeSets) { return (repo.serverSyncTree_.calcCompleteEventCache(path, excludeSets) || ChildrenNode.EMPTY_NODE); } /** * Sends any already-run transactions that aren't waiting for outstanding * transactions to complete. * * Externally it's called with no arguments, but it calls itself recursively * with a particular transactionQueueTree node to recurse through the tree. * * @param node transactionQueueTree node to start at. */ function repoSendReadyTransactions(repo, node) { if (node === void 0) { node = repo.transactionQueueTree_; } // Before recursing, make sure any completed transactions are removed. if (!node) { repoPruneCompletedTransactionsBelowNode(repo, node); } if (node.getValue() !== null) { var queue = repoBuildTransactionQueue(repo, node); assert(queue.length > 0, 'Sending zero length transaction queue'); var allRun = queue.every(function (transaction) { return transaction.status === 0 /* RUN */; }); // If they're all run (and not sent), we can send them. Else, we must wait. if (allRun) { repoSendTransactionQueue(repo, node.path(), queue); } } else if (node.hasChildren()) { node.forEachChild(function (childNode) { repoSendReadyTransactions(repo, childNode); }); } } /** * Given a list of run transactions, send them to the server and then handle * the result (success or failure). * * @param path The location of the queue. * @param queue Queue of transactions under the specified location. */ function repoSendTransactionQueue(repo, path, queue) { // Mark transactions as sent and increment retry count! var setsToIgnore = queue.map(function (txn) { return txn.currentWriteId; }); var latestState = repoGetLatestState(repo, path, setsToIgnore); var snapToSend = latestState; var latestHash = latestState.hash(); for (var i = 0; i < queue.length; i++) { var txn = queue[i]; assert(txn.status === 0 /* RUN */, 'tryToSendTransactionQueue_: items in queue should all be run.'); txn.status = 1 /* SENT */; txn.retryCount++; var relativePath = newRelativePath(path, txn.path); // If we've gotten to this point, the output snapshot must be defined. snapToSend = snapToSend.updateChild(relativePath /** @type {!Node} */, txn.currentOutputSnapshotRaw); } var dataToSend = snapToSend.val(true); var pathToSend = path; // Send the put. repo.server_.put(pathToSend.toString(), dataToSend, function (status) { repoLog(repo, 'transaction put response', { path: pathToSend.toString(), status: status }); var events = []; if (status === 'ok') { // Queue up the callbacks and fire them after cleaning up all of our // transaction state, since the callback could trigger more // transactions or sets. var callbacks = []; for (var i = 0; i < queue.length; i++) { queue[i].status = 2 /* COMPLETED */; events = events.concat(repo.serverSyncTree_.ackUserWrite(queue[i].currentWriteId)); if (queue[i].onComplete) { // We never unset the output snapshot, and given that this // transaction is complete, it should be set var node = queue[i].currentOutputSnapshotResolved; var ref = new Reference(repo, queue[i].path); var snapshot = new DataSnapshot(node, ref, PRIORITY_INDEX); callbacks.push(queue[i].onComplete.bind(null, null, true, snapshot)); } queue[i].unwatcher(); } // Now remove the completed transactions. repoPruneCompletedTransactionsBelowNode(repo, repo.transactionQueueTree_.subTree(path)); // There may be pending transactions that we can now send. repoSendReadyTransactions(repo, repo.transactionQueueTree_); eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events); // Finally, trigger onComplete callbacks. for (var i = 0; i < callbacks.length; i++) { exceptionGuard(callbacks[i]); } } else { // transactions are no longer sent. Update their status appropriately. if (status === 'datastale') { for (var i = 0; i < queue.length; i++) { if (queue[i].status === 3 /* SENT_NEEDS_ABORT */) { queue[i].status = 4 /* NEEDS_ABORT */; } else { queue[i].status = 0 /* RUN */; } } } else { warn('transaction at ' + pathToSend.toString() + ' failed: ' + status); for (var i = 0; i < queue.length; i++) { queue[i].status = 4 /* NEEDS_ABORT */; queue[i].abortReason = status; } } repoRerunTransactions(repo, path); } }, latestHash); } /** * Finds all transactions dependent on the data at changedPath and reruns them. * * Should be called any time cached data changes. * * Return the highest path that was affected by rerunning transactions. This * is the path at which events need to be raised for. * * @param changedPath The path in mergedData that changed. * @return The rootmost path that was affected by rerunning transactions. */ function repoRerunTransactions(repo, changedPath) { var rootMostTransactionNode = repoGetAncestorTransactionNode(repo, changedPath); var path = rootMostTransactionNode.path(); var queue = repoBuildTransactionQueue(repo, rootMostTransactionNode); repoRerunTransactionQueue(repo, queue, path); return path; } /** * Does all the work of rerunning transactions (as well as cleans up aborted * transactions and whatnot). * * @param queue The queue of transactions to run. * @param path The path the queue is for. */ function repoRerunTransactionQueue(repo, queue, path) { if (queue.length === 0) { return; // Nothing to do! } // Queue up the callbacks and fire them after cleaning up all of our // transaction state, since the callback could trigger more transactions or // sets. var callbacks = []; var events = []; // Ignore all of the sets we're going to re-run. var txnsToRerun = queue.filter(function (q) { return q.status === 0 /* RUN */; }); var setsToIgnore = txnsToRerun.map(function (q) { return q.currentWriteId; }); for (var i = 0; i < queue.length; i++) { var transaction = queue[i]; var relativePath = newRelativePath(path, transaction.path); var abortTransaction = false, abortReason = void 0; assert(relativePath !== null, 'rerunTransactionsUnderNode_: relativePath should not be null.'); if (transaction.status === 4 /* NEEDS_ABORT */) { abortTransaction = true; abortReason = transaction.abortReason; events = events.concat(repo.serverSyncTree_.ackUserWrite(transaction.currentWriteId, true)); } else if (transaction.status === 0 /* RUN */) { if (transaction.retryCount >= MAX_TRANSACTION_RETRIES) { abortTransaction = true; abortReason = 'maxretry'; events = events.concat(repo.serverSyncTree_.ackUserWrite(transaction.currentWriteId, true)); } else { // This code reruns a transaction var currentNode = repoGetLatestState(repo, transaction.path, setsToIgnore); transaction.currentInputSnapshot = currentNode; var newData = queue[i].update(currentNode.val()); if (newData !== undefined) { validateFirebaseData('transaction failed: Data returned ', newData, transaction.path); var newDataNode = nodeFromJSON$1(newData); var hasExplicitPriority = typeof newData === 'object' && newData != null && contains(newData, '.priority'); if (!hasExplicitPriority) { // Keep the old priority if there wasn't a priority explicitly specified. newDataNode = newDataNode.updatePriority(currentNode.getPriority()); } var oldWriteId = transaction.currentWriteId; var serverValues = repoGenerateServerValues(repo); var newNodeResolved = resolveDeferredValueSnapshot(newDataNode, currentNode, serverValues); transaction.currentOutputSnapshotRaw = newDataNode; transaction.currentOutputSnapshotResolved = newNodeResolved; transaction.currentWriteId = repoGetNextWriteId(repo); // Mutates setsToIgnore in place setsToIgnore.splice(setsToIgnore.indexOf(oldWriteId), 1); events = events.concat(repo.serverSyncTree_.applyUserOverwrite(transaction.path, newNodeResolved, transaction.currentWriteId, transaction.applyLocally)); events = events.concat(repo.serverSyncTree_.ackUserWrite(oldWriteId, true)); } else { abortTransaction = true; abortReason = 'nodata'; events = events.concat(repo.serverSyncTree_.ackUserWrite(transaction.currentWriteId, true)); } } } eventQueueRaiseEventsForChangedPath(repo.eventQueue_, path, events); events = []; if (abortTransaction) { // Abort. queue[i].status = 2 /* COMPLETED */; // Removing a listener can trigger pruning which can muck with // mergedData/visibleData (as it prunes data). So defer the unwatcher // until we're done. (function (unwatcher) { setTimeout(unwatcher, Math.floor(0)); })(queue[i].unwatcher); if (queue[i].onComplete) { if (abortReason === 'nodata') { var ref = new Reference(repo, queue[i].path); // We set this field immediately, so it's safe to cast to an actual snapshot var lastInput /** @type {!Node} */ = queue[i].currentInputSnapshot; var snapshot = new DataSnapshot(lastInput, ref, PRIORITY_INDEX); callbacks.push(queue[i].onComplete.bind(null, null, false, snapshot)); } else { callbacks.push(queue[i].onComplete.bind(null, new Error(abortReason), false, null)); } } } } // Clean up completed transactions. repoPruneCompletedTransactionsBelowNode(repo, repo.transactionQueueTree_); // Now fire callbacks, now that we're in a good, known state. for (var i = 0; i < callbacks.length; i++) { exceptionGuard(callbacks[i]); } // Try to send the transaction result to the server. repoSendReadyTransactions(repo, repo.transactionQueueTree_); } /** * Returns the rootmost ancestor node of the specified path that has a pending * transaction on it, or just returns the node for the given path if there are * no pending transactions on any ancestor. * * @param path The location to start at. * @return The rootmost node with a transaction. */ function repoGetAncestorTransactionNode(repo, path) { var front; // Start at the root and walk deeper into the tree towards path until we // find a node with pending transactions. var transactionNode = repo.transactionQueueTree_; front = pathGetFront(path); while (front !== null && transactionNode.getValue() === null) { transactionNode = transactionNode.subTree(front); path = pathPopFront(path); front = pathGetFront(path); } return transactionNode; } /** * Builds the queue of all transactions at or below the specified * transactionNode. * * @param transactionNode * @return The generated queue. */ function repoBuildTransactionQueue(repo, transactionNode) { // Walk any child transaction queues and aggregate them into a single queue. var transactionQueue = []; repoAggregateTransactionQueuesForNode(repo, transactionNode, transactionQueue); // Sort them by the order the transactions were created. transactionQueue.sort(function (a, b) { return a.order - b.order; }); return transactionQueue; } function repoAggregateTransactionQueuesForNode(repo, node, queue) { var nodeQueue = node.getValue(); if (nodeQueue !== null) { for (var i = 0; i < nodeQueue.length; i++) { queue.push(nodeQueue[i]); } } node.forEachChild(function (child) { repoAggregateTransactionQueuesForNode(repo, child, queue); }); } /** * Remove COMPLETED transactions at or below this node in the transactionQueueTree_. */ function repoPruneCompletedTransactionsBelowNode(repo, node) { var queue = node.getValue(); if (queue) { var to = 0; for (var from = 0; from < queue.length; from++) { if (queue[from].status !== 2 /* COMPLETED */) { queue[to] = queue[from]; to++; } } queue.length = to; node.setValue(queue.length > 0 ? queue : null); } node.forEachChild(function (childNode) { repoPruneCompletedTransactionsBelowNode(repo, childNode); }); } /** * Aborts all transactions on ancestors or descendants of the specified path. * Called when doing a set() or update() since we consider them incompatible * with transactions. * * @param path Path for which we want to abort related transactions. */ function repoAbortTransactions(repo, path) { var affectedPath = repoGetAncestorTransactionNode(repo, path).path(); var transactionNode = repo.transactionQueueTree_.subTree(path); transactionNode.forEachAncestor(function (node) { repoAbortTransactionsOnNode(repo, node); }); repoAbortTransactionsOnNode(repo, transactionNode); transactionNode.forEachDescendant(function (node) { repoAbortTransactionsOnNode(repo, node); }); return affectedPath; } /** * Abort transactions stored in this transaction queue node. * * @param node Node to abort transactions for. */ function repoAbortTransactionsOnNode(repo, node) { var queue = node.getValue(); if (queue !== null) { // Queue up the callbacks and fire them after cleaning up all of our // transaction state, since the callback could trigger more transactions // or sets. var callbacks = []; // Go through queue. Any already-sent transactions must be marked for // abort, while the unsent ones can be immediately aborted and removed. var events = []; var lastSent = -1; for (var i = 0; i < queue.length; i++) { if (queue[i].status === 3 /* SENT_NEEDS_ABORT */) ; else if (queue[i].status === 1 /* SENT */) { assert(lastSent === i - 1, 'All SENT items should be at beginning of queue.'); lastSent = i; // Mark transaction for abort when it comes back. queue[i].status = 3 /* SENT_NEEDS_ABORT */; queue[i].abortReason = 'set'; } else { assert(queue[i].status === 0 /* RUN */, 'Unexpected transaction status in abort'); // We can abort it immediately. queue[i].unwatcher(); events = events.concat(repo.serverSyncTree_.ackUserWrite(queue[i].currentWriteId, true)); if (queue[i].onComplete) { var snapshot = null; callbacks.push(queue[i].onComplete.bind(null, new Error('set'), false, snapshot)); } } } if (lastSent === -1) { // We're not waiting for any sent transactions. We can clear the queue. node.setValue(null); } else { // Remove the transactions we aborted. queue.length = lastSent + 1; } // Now fire the callbacks. eventQueueRaiseEventsForChangedPath(repo.eventQueue_, node.path(), events); for (var i = 0; i < callbacks.length; i++) { exceptionGuard(callbacks[i]); } } } /** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Abstraction around FirebaseApp's token fetching capabilities. */ var FirebaseAuthTokenProvider = /** @class */ (function () { function FirebaseAuthTokenProvider(app_, authProvider_) { var _this = this; this.app_ = app_; this.authProvider_ = authProvider_; this.auth_ = null; this.auth_ = authProvider_.getImmediate({ optional: true }); if (!this.auth_) { authProvider_.get().then(function (auth) { return (_this.auth_ = auth); }); } } FirebaseAuthTokenProvider.prototype.getToken = function (forceRefresh) { if (!this.auth_) { return Promise.resolve(null); } return this.auth_.getToken(forceRefresh).catch(function (error) { // TODO: Need to figure out all the cases this is raised and whether // this makes sense. if (error && error.code === 'auth/token-not-initialized') { log('Got auth/token-not-initialized error. Treating as null token.'); return null; } else { return Promise.reject(error); } }); }; FirebaseAuthTokenProvider.prototype.addTokenChangeListener = function (listener) { // TODO: We might want to wrap the listener and call it with no args to // avoid a leaky abstraction, but that makes removing the listener harder. if (this.auth_) { this.auth_.addAuthTokenListener(listener); } else { setTimeout(function () { return listener(null); }, 0); this.authProvider_ .get() .then(function (auth) { return auth.addAuthTokenListener(listener); }); } }; FirebaseAuthTokenProvider.prototype.removeTokenChangeListener = function (listener) { this.authProvider_ .get() .then(function (auth) { return auth.removeAuthTokenListener(listener); }); }; FirebaseAuthTokenProvider.prototype.notifyForInvalidToken = function () { var errorMessage = 'Provided authentication credentials for the app named "' + this.app_.name + '" are invalid. This usually indicates your app was not ' + 'initialized correctly. '; if ('credential' in this.app_.options) { 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.app_.options) { errorMessage += 'Make sure the "serviceAccount" property provided to initializeApp() ' + 'is authorized to access the specified "databaseURL" and is from the correct ' + 'project.'; } else { errorMessage += 'Make sure the "apiKey" and "databaseURL" properties provided to ' + 'initializeApp() match the values provided for your app at ' + 'https://console.firebase.google.com/.'; } warn(errorMessage); }; return FirebaseAuthTokenProvider; }()); /* Auth token provider that the Admin SDK uses to connect to the Emulator. */ var EmulatorAdminTokenProvider = /** @class */ (function () { function EmulatorAdminTokenProvider() { } EmulatorAdminTokenProvider.prototype.getToken = function (forceRefresh) { return Promise.resolve({ accessToken: EmulatorAdminTokenProvider.EMULATOR_AUTH_TOKEN }); }; EmulatorAdminTokenProvider.prototype.addTokenChangeListener = function (listener) { // Invoke the listener immediately to match the behavior in Firebase Auth // (see packages/auth/src/auth.js#L1807) listener(EmulatorAdminTokenProvider.EMULATOR_AUTH_TOKEN); }; EmulatorAdminTokenProvider.prototype.removeTokenChangeListener = function (listener) { }; EmulatorAdminTokenProvider.prototype.notifyForInvalidToken = function () { }; EmulatorAdminTokenProvider.EMULATOR_AUTH_TOKEN = 'owner'; return EmulatorAdminTokenProvider; }()); /** * @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 variable is also defined in the firebase node.js admin SDK. Before * modifying this definition, consult the definition in: * * https://github.com/firebase/firebase-admin-node * * and make sure the two are consistent. */ var FIREBASE_DATABASE_EMULATOR_HOST_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST'; var _staticInstance; /** * Creates and caches Repo instances. */ var RepoManager = /** @class */ (function () { function RepoManager() { this.repos_ = {}; /** * If true, new Repos will be created to use ReadonlyRestClient (for testing purposes). */ this.useRestClient_ = false; } RepoManager.getInstance = function () { if (!_staticInstance) { _staticInstance = new RepoManager(); } return _staticInstance; }; // TODO(koss): Remove these functions unless used in tests? RepoManager.prototype.interrupt = function () { var e_1, _a, e_2, _b; try { for (var _c = __values(Object.keys(this.repos_)), _d = _c.next(); !_d.done; _d = _c.next()) { var appName = _d.value; try { for (var _e = (e_2 = void 0, __values(Object.keys(this.repos_[appName]))), _f = _e.next(); !_f.done; _f = _e.next()) { var dbUrl = _f.value; repoInterrupt(this.repos_[appName][dbUrl]); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_f && !_f.done && (_b = _e.return)) _b.call(_e); } finally { if (e_2) throw e_2.error; } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_1) throw e_1.error; } } }; RepoManager.prototype.resume = function () { var e_3, _a, e_4, _b; try { for (var _c = __values(Object.keys(this.repos_)), _d = _c.next(); !_d.done; _d = _c.next()) { var appName = _d.value; try { for (var _e = (e_4 = void 0, __values(Object.keys(this.repos_[appName]))), _f = _e.next(); !_f.done; _f = _e.next()) { var dbUrl = _f.value; repoResume(this.repos_[appName][dbUrl]); } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (_f && !_f.done && (_b = _e.return)) _b.call(_e); } finally { if (e_4) throw e_4.error; } } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_3) throw e_3.error; } } }; /** * Update an existing repo in place to point to a new host/port. */ RepoManager.prototype.applyEmulatorSettings = function (repo, host, port) { repo.repoInfo_ = new RepoInfo(host + ":" + port, /* secure= */ false, repo.repoInfo_.namespace, repo.repoInfo_.webSocketOnly, repo.repoInfo_.nodeAdmin, repo.repoInfo_.persistenceKey, repo.repoInfo_.includeNamespaceInQueryParams); if (repo.repoInfo_.nodeAdmin) { repo.authTokenProvider_ = new EmulatorAdminTokenProvider(); } }; /** * This function should only ever be called to CREATE a new database instance. */ RepoManager.prototype.databaseFromApp = function (app, authProvider, url, nodeAdmin) { var dbUrl = url || app.options.databaseURL; if (dbUrl === undefined) { if (!app.options.projectId) { fatal("Can't determine Firebase Database URL. Be sure to include " + ' a Project ID when calling firebase.initializeApp().'); } log('Using default host for project ', app.options.projectId); dbUrl = app.options.projectId + "-default-rtdb.firebaseio.com"; } var parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); var repoInfo = parsedUrl.repoInfo; var isEmulator; var dbEmulatorHost = undefined; if (typeof process !== 'undefined') { dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR]; } if (dbEmulatorHost) { isEmulator = true; dbUrl = "http://" + dbEmulatorHost + "?ns=" + repoInfo.namespace; parsedUrl = parseRepoInfo(dbUrl, nodeAdmin); repoInfo = parsedUrl.repoInfo; } else { isEmulator = !parsedUrl.repoInfo.secure; } var authTokenProvider = nodeAdmin && isEmulator ? new EmulatorAdminTokenProvider() : new FirebaseAuthTokenProvider(app, authProvider); validateUrl('Invalid Firebase Database URL', 1, parsedUrl); if (!pathIsEmpty(parsedUrl.path)) { fatal('Database URL must point to the root of a Firebase Database ' + '(not including a child path).'); } var repo = this.createRepo(repoInfo, app, authTokenProvider); return repoGetDatabase(repo); }; /** * Remove the repo and make sure it is disconnected. * */ RepoManager.prototype.deleteRepo = function (repo) { var appRepos = safeGet(this.repos_, repo.app.name); // This should never happen... if (!appRepos || safeGet(appRepos, repo.key) !== repo) { fatal("Database " + repo.app.name + "(" + repo.repoInfo_ + ") has already been deleted."); } repoInterrupt(repo); delete appRepos[repo.key]; }; /** * Ensures a repo doesn't already exist and then creates one using the * provided app. * * @param repoInfo The metadata about the Repo * @return The Repo object for the specified server / repoName. */ RepoManager.prototype.createRepo = function (repoInfo, app, authTokenProvider) { var appRepos = safeGet(this.repos_, app.name); if (!appRepos) { appRepos = {}; this.repos_[app.name] = appRepos; } var repo = safeGet(appRepos, repoInfo.toURLString()); if (repo) { fatal('Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.'); } repo = new Repo(repoInfo, this.useRestClient_, app, authTokenProvider); appRepos[repoInfo.toURLString()] = repo; return repo; }; /** * Forces us to use ReadonlyRestClient instead of PersistentConnection for new Repos. */ RepoManager.prototype.forceRestClient = function (forceRestClient) { this.useRestClient_ = forceRestClient; }; return RepoManager; }()); /** * @license * Copyright 2020 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. */ /** * Class representing a Firebase Realtime Database. */ var FirebaseDatabase = /** @class */ (function () { function FirebaseDatabase(app, authProvider, databaseUrl) { this.app = app; this._delegate = RepoManager.getInstance().databaseFromApp(this.app, authProvider, databaseUrl); } /** * Modify this instance to communicate with the Realtime Database emulator. * *
Note: This method must be called before performing any other operation. * * @param host - the emulator host (ex: localhost) * @param port - the emulator port (ex: 8080) */ FirebaseDatabase.prototype.useEmulator = function (host, port) { this._delegate.useEmulator(host, port); }; FirebaseDatabase.prototype.ref = function (path) { return typeof path === 'string' ? this._delegate.ref(path) : this._delegate.ref(path); }; /** * Returns a reference to the root or the path specified in url. * We throw a exception if the url is not in the same domain as the * current repo. * @param url - A URL that refers to a database location. * @returns A Firebase reference. */ FirebaseDatabase.prototype.refFromURL = function (url) { return this._delegate.refFromURL(url); }; FirebaseDatabase.prototype.goOffline = function () { this._delegate.goOffline(); }; FirebaseDatabase.prototype.goOnline = function () { this._delegate.goOnline(); }; FirebaseDatabase.prototype._delete = function () { return this._delegate.INTERNAL.delete(); }; FirebaseDatabase.prototype._setDatabaseUrl = function (url) { }; FirebaseDatabase.ServerValue = Database.ServerValue; return FirebaseDatabase; }()); var ServerValue = Database.ServerValue; /** * Returns the instance of the Realtime Database SDK that is associated * with the provided {@link FirebaseApp}. Initializes a new instance with * with default settings if no instance exists or if the existing instance uses * a custom database URL. * * @param app - The {@link FirebaseApp} instance that the returned Realtime * Database instance is associated with. * @param url - The URL of the Realtime Database instance to connect to. If not * provided, the SDK connects to the default instance of the Firebase App. * @returns The `FirebaseDatabase` instance of the provided app. */ function getDatabase(app$1, url) { return app._getProvider(app$1, 'database-exp').getImmediate({ identifier: url }); } /** * @license * Copyright 2020 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. */ function registerDatabase() { app._registerComponent(new Component('database-exp', function (container, url) { var app = container.getProvider('app-exp').getImmediate(); var authProvider = container.getProvider('auth-internal'); return new FirebaseDatabase(app, authProvider, url); }, "PUBLIC" /* PUBLIC */).setMultipleInstances(true)); app.registerVersion('database-exp', version, 'node'); } registerDatabase(); exports.ServerValue = ServerValue; exports.enableLogging = enableLogging; exports.getDatabase = getDatabase; Object.defineProperty(exports, '__esModule', { value: true }); }).apply(this, arguments); } catch(err) { console.error(err); throw new Error( 'Cannot instantiate firebase-database.js - ' + 'be sure to load firebase-app.js first.' ); } }))); //# sourceMappingURL=firebase.js.map