(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) {
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;
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:
*
*
* - Promisifying setTimeout, so we can set a timeout in our Promise chain
* - Listening on a signal bus for abort events, just like the Fetch API
* - Failing in the same way the Fetch API fails, so timing out a live request and a throttled
* request appear the same.
*
*
* 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) {
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];
// This command initializes gtag.js and only needs to be called once for the entire web app,
// but since it is idempotent, we can call it multiple times.
// We keep it together with other initialization logic for better code structure.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
gtagCore('js', new Date());
configProperties = (_b = {},
// guard against developers accidentally setting properties with prefix `firebase_`
_b[ORIGIN_KEY] = 'firebase',
_b.update = true,
_b);
if (fid != null) {
configProperties[GA_FID_KEY] = fid;
}
// It should be the first config command called on this GA-ID
// Initialize this GA-ID and set FID on it using the gtag config API.
gtagCore("config" /* CONFIG */, dynamicConfig.measurementId, configProperties);
return [2 /*return*/, dynamicConfig.measurementId];
}
});
});
}
/**
* @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.
*/
/**
* Analytics Service class.
*/
var AnalyticsService = /** @class */ (function () {
function AnalyticsService(app) {
this.app = app;
}
AnalyticsService.prototype._delete = function () {
delete initializationPromisesMap[this.app.options.appId];
return Promise.resolve();
};
return AnalyticsService;
}());
/**
* Maps appId to full initialization promise. Wrapped gtag calls must wait on
* all or some of these, depending on the call's `send_to` param and the status
* of the dynamic config fetches (see below).
*/
var initializationPromisesMap = {};
/**
* List of dynamic config fetch promises. In certain cases, wrapped gtag calls
* wait on all these to be complete in order to determine if it can selectively
* wait for only certain initialization (FID) promises or if it must wait for all.
*/
var dynamicConfigPromisesList = [];
/**
* Maps fetched measurementIds to appId. Populated when the app's dynamic config
* fetch completes. If already populated, gtag config calls can use this to
* selectively wait for only this app's initialization promise (FID) instead of all
* initialization promises.
*/
var measurementIdToAppId = {};
/**
* Name for window global data layer array used by GA: defaults to 'dataLayer'.
*/
var dataLayerName = 'dataLayer';
/**
* Name for window global gtag function used by GA: defaults to 'gtag'.
*/
var gtagName = 'gtag';
/**
* Reproduction of standard gtag function or reference to existing
* gtag function on window object.
*/
var gtagCoreFunction;
/**
* Wrapper around gtag function that ensures FID is sent with all
* relevant event and config calls.
*/
var wrappedGtagFunction;
/**
* Flag to ensure page initialization steps (creation or wrapping of
* dataLayer and gtag script) are only run once per page load.
*/
var globalInitDone = false;
/**
* Configures Firebase Analytics to use custom `gtag` or `dataLayer` names.
* Intended to be used if `gtag.js` script has been installed on
* this page independently of Firebase Analytics, and is using non-default
* names for either the `gtag` function or for `dataLayer`.
* Must be called before calling `getAnalytics()` or it won't
* have any effect.
*
* @public
*
* @param options - Custom gtag and dataLayer names.
*/
function settings(options) {
if (globalInitDone) {
throw ERROR_FACTORY$1.create("already-initialized" /* ALREADY_INITIALIZED */);
}
if (options.dataLayerName) {
dataLayerName = options.dataLayerName;
}
if (options.gtagName) {
gtagName = options.gtagName;
}
}
/**
* Returns true if no environment mismatch is found.
* If environment mismatches are found, throws an INVALID_ANALYTICS_CONTEXT
* error that also lists details for each mismatch found.
*/
function warnOnBrowserContextMismatch() {
var mismatchedEnvMessages = [];
if (isBrowserExtension()) {
mismatchedEnvMessages.push('This is a browser extension environment.');
}
if (!areCookiesEnabled()) {
mismatchedEnvMessages.push('Cookies are not available.');
}
if (mismatchedEnvMessages.length > 0) {
var details = mismatchedEnvMessages
.map(function (message, index) { return "(" + (index + 1) + ") " + message; })
.join(' ');
var err = ERROR_FACTORY$1.create("invalid-analytics-context" /* INVALID_ANALYTICS_CONTEXT */, {
errorInfo: details
});
logger.warn(err.message);
}
}
/**
* Analytics instance factory.
* @internal
*/
function factory(app, installations) {
warnOnBrowserContextMismatch();
var appId = app.options.appId;
if (!appId) {
throw ERROR_FACTORY$1.create("no-app-id" /* NO_APP_ID */);
}
if (!app.options.apiKey) {
if (app.options.measurementId) {
logger.warn("The \"apiKey\" field is empty in the local Firebase config. This is needed to fetch the latest" +
(" measurement ID for this Firebase app. Falling back to the measurement ID " + app.options.measurementId) +
" provided in the \"measurementId\" field in the local Firebase config.");
}
else {
throw ERROR_FACTORY$1.create("no-api-key" /* NO_API_KEY */);
}
}
if (initializationPromisesMap[appId] != null) {
throw ERROR_FACTORY$1.create("already-exists" /* ALREADY_EXISTS */, {
id: appId
});
}
if (!globalInitDone) {
// Steps here should only be done once per page: creation or wrapping
// of dataLayer and global gtag function.
// 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.
* @private
* @return {Element}
*/
FirebaseIFrameScriptHolder.createIFrame_ = function () {
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
// This is necessary in order to initialize the document inside the iframe
if (document.body) {
document.body.appendChild(iframe);
try {
// If document.domain has been modified in IE, this will throw an error, and we need to set the
// domain of the iframe's document manually. We can do this via a javascript: url as the src attribute
// Also note that we must do this *after* the iframe has been appended to the page. Otherwise it doesn't work.
var a = iframe.contentWindow.document;
if (!a) {
// Apologies for the log-spam, I need to do something to keep closure from optimizing out the assignment above.
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 {!string} id - The ID of this connection
* @param {!string} pw - The password for this connection
*/
FirebaseIFrameScriptHolder.prototype.startLongPoll = function (id, pw) {
this.myID = id;
this.myPW = pw;
this.alive = true;
//send the initial request. If there are requests queued, make sure that we transmit as many as we are currently able to.
while (this.newRequest_()) { }
};
/**
* This is called any time someone might want a script tag to be added. It adds a script tag when there aren't
* too many outstanding requests and we are still alive.
*
* If there are outstanding packet segments to send, it sends one. If there aren't, it sends a long-poll anyways if
* needed.
*/
FirebaseIFrameScriptHolder.prototype.newRequest_ = function () {
// We keep one outstanding request open all the time to receive data, but if we need to send data
// (pendingSegs.length > 0) then we create a new request to send the data. The server will automatically
// close the old request.
if (this.alive &&
this.sendNewPolls &&
this.outstandingRequests.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 {!string} url - The URL of the script tag.
* @param {!number} serial - The serial number of the request.
* @private
*/
FirebaseIFrameScriptHolder.prototype.addLongPollTag_ = function (url, serial) {
var _this = this;
//remember that we sent this request.
this.outstandingRequests.add(serial);
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 {!string} url - The URL for the script tag source.
* @param {!function()} loadCB - A callback to be triggered once the script has loaded.
*/
FirebaseIFrameScriptHolder.prototype.addTag = function (url, loadCB) {
var _this = this;
{
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.
* @constructor
* @implements {Transport}
*/
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} repoInfo The info for the websocket endpoint.
* @param {string=} transportSessionId Optional transportSessionId if this is connecting to an existing transport
* session
* @param {string=} lastSessionId Optional lastSessionId if there was a previous connection
* @return {string} connection url
* @private
*/
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.
* @return {boolean}
*/
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 {number} frameCount The number of frames we are expecting from the server
* @private
*/
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
* @param {!String} data
* @return {?String} Any remaining data to be process, or null if there is none
* @private
*/
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 {Object} 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 {string} str String to send.
* @private
*/
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."
* @type {number}
*/
WebSocketConnection.responsesRequiredToBeHealthy = 2;
/**
* Time to wait for the connection te become healthy before giving up.
* @type {number}
*/
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.
* @constructor
*/
var TransportManager = /** @class */ (function () {
/**
* @param {!RepoInfo} repoInfo Metadata around the namespace we're connecting to
*/
function TransportManager(repoInfo) {
this.initTransports_(repoInfo);
}
Object.defineProperty(TransportManager, "ALL_TRANSPORTS", {
/**
* @const
* @type {!Array.}
*/
get: function () {
return [BrowserPollConnection, WebSocketConnection];
},
enumerable: false,
configurable: true
});
/**
* @param {!RepoInfo} repoInfo
* @private
*/
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 {function(new:Transport, !string, !RepoInfo, string=, string=)} 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 {?function(new:Transport, function(),function(), string=)} 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.
*
* @constructor
*/
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
* @private
*/
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));
}
};
/**
* @return {!string}
* @private
*/
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 {Object} 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 {Object} handshake The handshake data returned from the server
* @private
*/
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 {boolean} everConnected Whether or not the connection ever reached a server. Used to determine if
* we should flush the host cache
* @private
*/
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. .firebaseio.com
this.repoInfo_.internalHost = this.repoInfo_.host;
}
}
else if (this.state_ === 1 /* CONNECTED */) {
this.log_('Realtime connection lost.');
}
this.close();
};
/**
*
* @param {string} reason
* @private
*/
Connection.prototype.onConnectionShutdown_ = function (reason) {
this.log_('Connection shutdown command received. Shutting down...');
if (this.onKill_) {
this.onKill_(reason);
this.onKill_ = null;
}
// We intentionally don't want to fire onDisconnect (kill is a different case),
// so clear the callback.
this.onDisconnect_ = null;
this.close();
};
Connection.prototype.sendData_ = function (data) {
if (this.state_ !== 1 /* CONNECTED */) {
throw 'Connection is not connected';
}
else {
this.tx_.send(data);
}
};
/**
* Cleans up this connection, calling the appropriate callbacks
*/
Connection.prototype.close = function () {
if (this.state_ !== 2 /* DISCONNECTED */) {
this.log_('Closing realtime connection.');
this.state_ = 2 /* DISCONNECTED */;
this.closeConnections_();
if (this.onDisconnect_) {
this.onDisconnect_();
this.onDisconnect_ = null;
}
}
};
/**
*
* @private
*/
Connection.prototype.closeConnections_ = function () {
this.log_('Shutting down all connections');
if (this.conn_) {
this.conn_.close();
this.conn_ = null;
}
if (this.secondaryConn_) {
this.secondaryConn_.close();
this.secondaryConn_ = null;
}
if (this.healthyTimeout_) {
clearTimeout(this.healthyTimeout_);
this.healthyTimeout_ = null;
}
};
return Connection;
}());
/**
* @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.
*/
/**
* Interface defining the set of actions that can be performed against the Firebase server
* (basically corresponds to our wire protocol).
*
* @interface
*/
var ServerActions = /** @class */ (function () {
function ServerActions() {
}
/**
* @param {string} pathString
* @param {*} data
* @param {function(string, string)=} onComplete
* @param {string=} hash
*/
ServerActions.prototype.put = function (pathString, data, onComplete, hash) { };
/**
* @param {string} pathString
* @param {*} data
* @param {function(string, ?string)} onComplete
* @param {string=} hash
*/
ServerActions.prototype.merge = function (pathString, data, onComplete, hash) { };
/**
* Refreshes the auth token for the current connection.
* @param {string} token The authentication token
*/
ServerActions.prototype.refreshAuthToken = function (token) { };
/**
* @param {string} pathString
* @param {*} data
* @param {function(string, string)=} onComplete
*/
ServerActions.prototype.onDisconnectPut = function (pathString, data, onComplete) { };
/**
* @param {string} pathString
* @param {*} data
* @param {function(string, string)=} onComplete
*/
ServerActions.prototype.onDisconnectMerge = function (pathString, data, onComplete) { };
/**
* @param {string} pathString
* @param {function(string, string)=} onComplete
*/
ServerActions.prototype.onDisconnectCancel = function (pathString, onComplete) { };
/**
* @param {Object.} stats
*/
ServerActions.prototype.reportStats = function (stats) { };
return ServerActions;
}());
/**
* @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 RECONNECT_MIN_DELAY = 1000;
var RECONNECT_MAX_DELAY_DEFAULT = 60 * 5 * 1000; // 5 minutes in milliseconds (Case: 1858)
var GET_CONNECT_TIMEOUT = 3 * 1000;
var RECONNECT_MAX_DELAY_FOR_ADMINS = 30 * 1000; // 30 seconds for admin clients (likely to be a backend server)
var RECONNECT_DELAY_MULTIPLIER = 1.3;
var RECONNECT_DELAY_RESET_TIMEOUT = 30000; // Reset delay back to MIN_DELAY after being connected for 30sec.
var SERVER_KILL_INTERRUPT_REASON = 'server_kill';
// If auth fails repeatedly, we'll assume something is wrong and log a warning / back off.
var INVALID_AUTH_TOKEN_THRESHOLD = 3;
/**
* Firebase connection. Abstracts wire protocol and handles reconnecting.
*
* NOTE: All JSON objects sent to the realtime connection must have property names enclosed
* in quotes to make sure the closure compiler does not minify them.
*/
var PersistentConnection = /** @class */ (function (_super) {
__extends$1(PersistentConnection, _super);
/**
* @implements {ServerActions}
* @param repoInfo_ Data about the namespace we are connecting to
* @param applicationId_ The Firebase App ID for this project
* @param onDataUpdate_ A callback for new data from the server
*/
function PersistentConnection(repoInfo_, applicationId_, onDataUpdate_, onConnectStatus_, onServerInfoUpdate_, authTokenProvider_, authOverride_) {
var _this = _super.call(this) || this;
_this.repoInfo_ = repoInfo_;
_this.applicationId_ = applicationId_;
_this.onDataUpdate_ = onDataUpdate_;
_this.onConnectStatus_ = onConnectStatus_;
_this.onServerInfoUpdate_ = onServerInfoUpdate_;
_this.authTokenProvider_ = authTokenProvider_;
_this.authOverride_ = authOverride_;
// Used for diagnostic logging.
_this.id = PersistentConnection.nextPersistentConnectionId_++;
_this.log_ = logWrapper('p:' + _this.id + ':');
_this.interruptReasons_ = {};
/** Map> */
_this.listens = new Map();
_this.outstandingPuts_ = [];
_this.outstandingGets_ = [];
_this.outstandingPutCount_ = 0;
_this.outstandingGetCount_ = 0;
_this.onDisconnectRequestQueue_ = [];
_this.connected_ = false;
_this.reconnectDelay_ = RECONNECT_MIN_DELAY;
_this.maxReconnectDelay_ = RECONNECT_MAX_DELAY_DEFAULT;
_this.securityDebugCallback_ = null;
_this.lastSessionId = null;
_this.establishConnectionTimer_ = null;
_this.visible_ = false;
// Before we get connected, we keep a queue of pending messages to send.
_this.requestCBHash_ = {};
_this.requestNumber_ = 0;
_this.realtime_ = null;
_this.authToken_ = null;
_this.forceTokenRefresh_ = false;
_this.invalidAuthTokenCount_ = 0;
_this.firstConnection_ = true;
_this.lastConnectionAttemptTime_ = null;
_this.lastConnectionEstablishedTime_ = null;
if (authOverride_ && !isNodeSdk()) {
throw new Error('Auth override specified in options, but not supported on non Node.js platforms');
}
_this.scheduleConnect_(0);
VisibilityMonitor.getInstance().on('visible', _this.onVisible_, _this);
if (repoInfo_.host.indexOf('fblocal') === -1) {
OnlineMonitor.getInstance().on('online', _this.onOnline_, _this);
}
return _this;
}
PersistentConnection.prototype.sendRequest = function (action, body, onResponse) {
var curReqNum = ++this.requestNumber_;
var msg = { r: curReqNum, a: action, b: body };
this.log_(stringify(msg));
assert(this.connected_, "sendRequest call when we're not connected not allowed.");
this.realtime_.sendRequest(msg);
if (onResponse) {
this.requestCBHash_[curReqNum] = onResponse;
}
};
PersistentConnection.prototype.get = function (query) {
var _this = this;
var deferred = new Deferred();
var request = {
p: query.path.toString(),
q: query.queryObject()
};
var outstandingGet = {
action: 'g',
request: request,
onComplete: function (message) {
var payload = message['d'];
if (message['s'] === 'ok') {
_this.onDataUpdate_(request['p'], payload,
/*isMerge*/ false,
/*tag*/ null);
deferred.resolve(payload);
}
else {
deferred.reject(payload);
}
}
};
this.outstandingGets_.push(outstandingGet);
this.outstandingGetCount_++;
var index = this.outstandingGets_.length - 1;
if (!this.connected_) {
setTimeout(function () {
var get = _this.outstandingGets_[index];
if (get === undefined || outstandingGet !== get) {
return;
}
delete _this.outstandingGets_[index];
_this.outstandingGetCount_--;
if (_this.outstandingGetCount_ === 0) {
_this.outstandingGets_ = [];
}
_this.log_('get ' + index + ' timed out on connection');
deferred.reject(new Error('Client is offline.'));
}, GET_CONNECT_TIMEOUT);
}
if (this.connected_) {
this.sendGet_(index);
}
return deferred.promise;
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.listen = function (query, currentHashFn, tag, onComplete) {
var queryId = query.queryIdentifier();
var pathString = query.path.toString();
this.log_('Listen called for ' + pathString + ' ' + queryId);
if (!this.listens.has(pathString)) {
this.listens.set(pathString, new Map());
}
assert(query.getQueryParams().isDefault() ||
!query.getQueryParams().loadsAllData(), 'listen() called for non-default but complete query');
assert(!this.listens.get(pathString).has(queryId), 'listen() called twice for same path/queryId.');
var listenSpec = {
onComplete: onComplete,
hashFn: currentHashFn,
query: query,
tag: tag
};
this.listens.get(pathString).set(queryId, listenSpec);
if (this.connected_) {
this.sendListen_(listenSpec);
}
};
PersistentConnection.prototype.sendGet_ = function (index) {
var _this = this;
var get = this.outstandingGets_[index];
this.sendRequest('g', get.request, function (message) {
delete _this.outstandingGets_[index];
_this.outstandingGetCount_--;
if (_this.outstandingGetCount_ === 0) {
_this.outstandingGets_ = [];
}
if (get.onComplete) {
get.onComplete(message);
}
});
};
PersistentConnection.prototype.sendListen_ = function (listenSpec) {
var _this = this;
var query = listenSpec.query;
var pathString = query.path.toString();
var queryId = query.queryIdentifier();
this.log_('Listen on ' + pathString + ' for ' + queryId);
var req = { /*path*/ p: pathString };
var action = 'q';
// Only bother to send query if it's non-default.
if (listenSpec.tag) {
req['q'] = query.queryObject();
req['t'] = listenSpec.tag;
}
req[ /*hash*/'h'] = listenSpec.hashFn();
this.sendRequest(action, req, function (message) {
var payload = message[ /*data*/'d'];
var status = message[ /*status*/'s'];
// print warnings in any case...
PersistentConnection.warnOnListenWarnings_(payload, query);
var currentListenSpec = _this.listens.get(pathString) &&
_this.listens.get(pathString).get(queryId);
// only trigger actions if the listen hasn't been removed and readded
if (currentListenSpec === listenSpec) {
_this.log_('listen response', message);
if (status !== 'ok') {
_this.removeListen_(pathString, queryId);
}
if (listenSpec.onComplete) {
listenSpec.onComplete(status, payload);
}
}
});
};
PersistentConnection.warnOnListenWarnings_ = function (payload, query) {
if (payload && typeof payload === 'object' && contains(payload, 'w')) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var warnings = safeGet(payload, 'w');
if (Array.isArray(warnings) && ~warnings.indexOf('no_index')) {
var indexSpec = '".indexOn": "' + query.getQueryParams().getIndex().toString() + '"';
var indexPath = query.path.toString();
warn("Using an unspecified index. Your data will be downloaded and " +
("filtered on the client. Consider adding " + indexSpec + " at ") +
(indexPath + " to your security rules for better performance."));
}
}
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.refreshAuthToken = function (token) {
this.authToken_ = token;
this.log_('Auth token refreshed');
if (this.authToken_) {
this.tryAuth();
}
else {
//If we're connected we want to let the server know to unauthenticate us. If we're not connected, simply delete
//the credential so we dont become authenticated next time we connect.
if (this.connected_) {
this.sendRequest('unauth', {}, function () { });
}
}
this.reduceReconnectDelayIfAdminCredential_(token);
};
PersistentConnection.prototype.reduceReconnectDelayIfAdminCredential_ = function (credential) {
// NOTE: This isn't intended to be bulletproof (a malicious developer can always just modify the client).
// Additionally, we don't bother resetting the max delay back to the default if auth fails / expires.
var isFirebaseSecret = credential && credential.length === 40;
if (isFirebaseSecret || isAdmin(credential)) {
this.log_('Admin auth credential detected. Reducing max reconnect time.');
this.maxReconnectDelay_ = RECONNECT_MAX_DELAY_FOR_ADMINS;
}
};
/**
* Attempts to authenticate with the given credentials. If the authentication attempt fails, it's triggered like
* a auth revoked (the connection is closed).
*/
PersistentConnection.prototype.tryAuth = function () {
var _this = this;
if (this.connected_ && this.authToken_) {
var token_1 = this.authToken_;
var authMethod = isValidFormat(token_1) ? 'auth' : 'gauth';
var requestData = { cred: token_1 };
if (this.authOverride_ === null) {
requestData['noauth'] = true;
}
else if (typeof this.authOverride_ === 'object') {
requestData['authvar'] = this.authOverride_;
}
this.sendRequest(authMethod, requestData, function (res) {
var status = res[ /*status*/'s'];
var data = res[ /*data*/'d'] || 'error';
if (_this.authToken_ === token_1) {
if (status === 'ok') {
_this.invalidAuthTokenCount_ = 0;
}
else {
// Triggers reconnect and force refresh for auth token
_this.onAuthRevoked_(status, data);
}
}
});
}
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.unlisten = function (query, tag) {
var pathString = query.path.toString();
var queryId = query.queryIdentifier();
this.log_('Unlisten called for ' + pathString + ' ' + queryId);
assert(query.getQueryParams().isDefault() ||
!query.getQueryParams().loadsAllData(), 'unlisten() called for non-default but complete query');
var listen = this.removeListen_(pathString, queryId);
if (listen && this.connected_) {
this.sendUnlisten_(pathString, queryId, query.queryObject(), tag);
}
};
PersistentConnection.prototype.sendUnlisten_ = function (pathString, queryId, queryObj, tag) {
this.log_('Unlisten on ' + pathString + ' for ' + queryId);
var req = { /*path*/ p: pathString };
var action = 'n';
// Only bother sending queryId if it's non-default.
if (tag) {
req['q'] = queryObj;
req['t'] = tag;
}
this.sendRequest(action, req);
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.onDisconnectPut = function (pathString, data, onComplete) {
if (this.connected_) {
this.sendOnDisconnect_('o', pathString, data, onComplete);
}
else {
this.onDisconnectRequestQueue_.push({
pathString: pathString,
action: 'o',
data: data,
onComplete: onComplete
});
}
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.onDisconnectMerge = function (pathString, data, onComplete) {
if (this.connected_) {
this.sendOnDisconnect_('om', pathString, data, onComplete);
}
else {
this.onDisconnectRequestQueue_.push({
pathString: pathString,
action: 'om',
data: data,
onComplete: onComplete
});
}
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.onDisconnectCancel = function (pathString, onComplete) {
if (this.connected_) {
this.sendOnDisconnect_('oc', pathString, null, onComplete);
}
else {
this.onDisconnectRequestQueue_.push({
pathString: pathString,
action: 'oc',
data: null,
onComplete: onComplete
});
}
};
PersistentConnection.prototype.sendOnDisconnect_ = function (action, pathString, data, onComplete) {
var request = { /*path*/ p: pathString, /*data*/ d: data };
this.log_('onDisconnect ' + action, request);
this.sendRequest(action, request, function (response) {
if (onComplete) {
setTimeout(function () {
onComplete(response[ /*status*/'s'], response[ /* data */'d']);
}, Math.floor(0));
}
});
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.put = function (pathString, data, onComplete, hash) {
this.putInternal('p', pathString, data, onComplete, hash);
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.merge = function (pathString, data, onComplete, hash) {
this.putInternal('m', pathString, data, onComplete, hash);
};
PersistentConnection.prototype.putInternal = function (action, pathString, data, onComplete, hash) {
var request = {
/*path*/ p: pathString,
/*data*/ d: data
};
if (hash !== undefined) {
request[ /*hash*/'h'] = hash;
}
// TODO: Only keep track of the most recent put for a given path?
this.outstandingPuts_.push({
action: action,
request: request,
onComplete: onComplete
});
this.outstandingPutCount_++;
var index = this.outstandingPuts_.length - 1;
if (this.connected_) {
this.sendPut_(index);
}
else {
this.log_('Buffering put: ' + pathString);
}
};
PersistentConnection.prototype.sendPut_ = function (index) {
var _this = this;
var action = this.outstandingPuts_[index].action;
var request = this.outstandingPuts_[index].request;
var onComplete = this.outstandingPuts_[index].onComplete;
this.outstandingPuts_[index].queued = this.connected_;
this.sendRequest(action, request, function (message) {
_this.log_(action + ' response', message);
delete _this.outstandingPuts_[index];
_this.outstandingPutCount_--;
// Clean up array occasionally.
if (_this.outstandingPutCount_ === 0) {
_this.outstandingPuts_ = [];
}
if (onComplete) {
onComplete(message[ /*status*/'s'], message[ /* data */'d']);
}
});
};
/**
* @inheritDoc
*/
PersistentConnection.prototype.reportStats = function (stats) {
var _this = this;
// If we're not connected, we just drop the stats.
if (this.connected_) {
var request = { /*counters*/ c: stats };
this.log_('reportStats', request);
this.sendRequest(/*stats*/ 's', request, function (result) {
var status = result[ /*status*/'s'];
if (status !== 'ok') {
var errorReason = result[ /* data */'d'];
_this.log_('reportStats', 'Error sending stats: ' + errorReason);
}
});
}
};
PersistentConnection.prototype.onDataMessage_ = function (message) {
if ('r' in message) {
// this is a response
this.log_('from server: ' + stringify(message));
var reqNum = message['r'];
var onResponse = this.requestCBHash_[reqNum];
if (onResponse) {
delete this.requestCBHash_[reqNum];
onResponse(message[ /*body*/'b']);
}
}
else if ('error' in message) {
throw 'A server-side error has occurred: ' + message['error'];
}
else if ('a' in message) {
// a and b are action and body, respectively
this.onDataPush_(message['a'], message['b']);
}
};
PersistentConnection.prototype.onDataPush_ = function (action, body) {
this.log_('handleServerMessage', action, body);
if (action === 'd') {
this.onDataUpdate_(body[ /*path*/'p'], body[ /*data*/'d'],
/*isMerge*/ false, body['t']);
}
else if (action === 'm') {
this.onDataUpdate_(body[ /*path*/'p'], body[ /*data*/'d'],
/*isMerge=*/ true, body['t']);
}
else if (action === 'c') {
this.onListenRevoked_(body[ /*path*/'p'], body[ /*query*/'q']);
}
else if (action === 'ac') {
this.onAuthRevoked_(body[ /*status code*/'s'], body[ /* explanation */'d']);
}
else if (action === 'sd') {
this.onSecurityDebugPacket_(body);
}
else {
error('Unrecognized action received from server: ' +
stringify(action) +
'\nAre you using the latest client?');
}
};
PersistentConnection.prototype.onReady_ = function (timestamp, sessionId) {
this.log_('connection ready');
this.connected_ = true;
this.lastConnectionEstablishedTime_ = new Date().getTime();
this.handleTimestamp_(timestamp);
this.lastSessionId = sessionId;
if (this.firstConnection_) {
this.sendConnectStats_();
}
this.restoreState_();
this.firstConnection_ = false;
this.onConnectStatus_(true);
};
PersistentConnection.prototype.scheduleConnect_ = function (timeout) {
var _this = this;
assert(!this.realtime_, "Scheduling a connect when we're already connected/ing?");
if (this.establishConnectionTimer_) {
clearTimeout(this.establishConnectionTimer_);
}
// NOTE: Even when timeout is 0, it's important to do a setTimeout to work around an infuriating "Security Error" in
// Firefox when trying to write to our long-polling iframe in some scenarios (e.g. Forge or our unit tests).
this.establishConnectionTimer_ = setTimeout(function () {
_this.establishConnectionTimer_ = null;
_this.establishConnection_();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}, Math.floor(timeout));
};
PersistentConnection.prototype.onVisible_ = function (visible) {
// NOTE: Tabbing away and back to a window will defeat our reconnect backoff, but I think that's fine.
if (visible &&
!this.visible_ &&
this.reconnectDelay_ === this.maxReconnectDelay_) {
this.log_('Window became visible. Reducing delay.');
this.reconnectDelay_ = RECONNECT_MIN_DELAY;
if (!this.realtime_) {
this.scheduleConnect_(0);
}
}
this.visible_ = visible;
};
PersistentConnection.prototype.onOnline_ = function (online) {
if (online) {
this.log_('Browser went online.');
this.reconnectDelay_ = RECONNECT_MIN_DELAY;
if (!this.realtime_) {
this.scheduleConnect_(0);
}
}
else {
this.log_('Browser went offline. Killing connection.');
if (this.realtime_) {
this.realtime_.close();
}
}
};
PersistentConnection.prototype.onRealtimeDisconnect_ = function () {
this.log_('data client disconnected');
this.connected_ = false;
this.realtime_ = null;
// Since we don't know if our sent transactions succeeded or not, we need to cancel them.
this.cancelSentTransactions_();
// Clear out the pending requests.
this.requestCBHash_ = {};
if (this.shouldReconnect_()) {
if (!this.visible_) {
this.log_("Window isn't visible. Delaying reconnect.");
this.reconnectDelay_ = this.maxReconnectDelay_;
this.lastConnectionAttemptTime_ = new Date().getTime();
}
else if (this.lastConnectionEstablishedTime_) {
// If we've been connected long enough, reset reconnect delay to minimum.
var timeSinceLastConnectSucceeded = new Date().getTime() - this.lastConnectionEstablishedTime_;
if (timeSinceLastConnectSucceeded > RECONNECT_DELAY_RESET_TIMEOUT) {
this.reconnectDelay_ = RECONNECT_MIN_DELAY;
}
this.lastConnectionEstablishedTime_ = null;
}
var timeSinceLastConnectAttempt = new Date().getTime() - this.lastConnectionAttemptTime_;
var reconnectDelay = Math.max(0, this.reconnectDelay_ - timeSinceLastConnectAttempt);
reconnectDelay = Math.random() * reconnectDelay;
this.log_('Trying to reconnect in ' + reconnectDelay + 'ms');
this.scheduleConnect_(reconnectDelay);
// Adjust reconnect delay for next time.
this.reconnectDelay_ = Math.min(this.maxReconnectDelay_, this.reconnectDelay_ * RECONNECT_DELAY_MULTIPLIER);
}
this.onConnectStatus_(false);
};
PersistentConnection.prototype.establishConnection_ = function () {
var _this = this;
if (this.shouldReconnect_()) {
this.log_('Making a connection attempt');
this.lastConnectionAttemptTime_ = new Date().getTime();
this.lastConnectionEstablishedTime_ = null;
var onDataMessage_1 = this.onDataMessage_.bind(this);
var onReady_1 = this.onReady_.bind(this);
var onDisconnect_1 = this.onRealtimeDisconnect_.bind(this);
var connId_1 = this.id + ':' + PersistentConnection.nextConnectionId_++;
var self_1 = this;
var lastSessionId_1 = this.lastSessionId;
var canceled_1 = false;
var connection_1 = null;
var closeFn_1 = function () {
if (connection_1) {
connection_1.close();
}
else {
canceled_1 = true;
onDisconnect_1();
}
};
var sendRequestFn = function (msg) {
assert(connection_1, "sendRequest call when we're not connected not allowed.");
connection_1.sendRequest(msg);
};
this.realtime_ = {
close: closeFn_1,
sendRequest: sendRequestFn
};
var forceRefresh = this.forceTokenRefresh_;
this.forceTokenRefresh_ = false;
// First fetch auth token, and establish connection after fetching the token was successful
this.authTokenProvider_
.getToken(forceRefresh)
.then(function (result) {
if (!canceled_1) {
log('getToken() completed. Creating connection.');
self_1.authToken_ = result && result.accessToken;
connection_1 = new Connection(connId_1, self_1.repoInfo_, self_1.applicationId_, onDataMessage_1, onReady_1, onDisconnect_1,
/* onKill= */ function (reason) {
warn(reason + ' (' + self_1.repoInfo_.toString() + ')');
self_1.interrupt(SERVER_KILL_INTERRUPT_REASON);
}, lastSessionId_1);
}
else {
log('getToken() completed but was canceled');
}
})
.then(null, function (error) {
self_1.log_('Failed to get token: ' + error);
if (!canceled_1) {
if (_this.repoInfo_.nodeAdmin) {
// This may be a critical error for the Admin Node.js SDK, so log a warning.
// But getToken() may also just have temporarily failed, so we still want to
// continue retrying.
warn(error);
}
closeFn_1();
}
});
}
};
PersistentConnection.prototype.interrupt = function (reason) {
log('Interrupting connection for reason: ' + reason);
this.interruptReasons_[reason] = true;
if (this.realtime_) {
this.realtime_.close();
}
else {
if (this.establishConnectionTimer_) {
clearTimeout(this.establishConnectionTimer_);
this.establishConnectionTimer_ = null;
}
if (this.connected_) {
this.onRealtimeDisconnect_();
}
}
};
PersistentConnection.prototype.resume = function (reason) {
log('Resuming connection for reason: ' + reason);
delete this.interruptReasons_[reason];
if (isEmpty(this.interruptReasons_)) {
this.reconnectDelay_ = RECONNECT_MIN_DELAY;
if (!this.realtime_) {
this.scheduleConnect_(0);
}
}
};
PersistentConnection.prototype.handleTimestamp_ = function (timestamp) {
var delta = timestamp - new Date().getTime();
this.onServerInfoUpdate_({ serverTimeOffset: delta });
};
PersistentConnection.prototype.cancelSentTransactions_ = function () {
for (var i = 0; i < this.outstandingPuts_.length; i++) {
var put = this.outstandingPuts_[i];
if (put && /*hash*/ 'h' in put.request && put.queued) {
if (put.onComplete) {
put.onComplete('disconnect');
}
delete this.outstandingPuts_[i];
this.outstandingPutCount_--;
}
}
// Clean up array occasionally.
if (this.outstandingPutCount_ === 0) {
this.outstandingPuts_ = [];
}
};
PersistentConnection.prototype.onListenRevoked_ = function (pathString, query) {
// Remove the listen and manufacture a "permission_denied" error for the failed listen.
var queryId;
if (!query) {
queryId = 'default';
}
else {
queryId = query.map(function (q) { return ObjectToUniqueKey(q); }).join('$');
}
var listen = this.removeListen_(pathString, queryId);
if (listen && listen.onComplete) {
listen.onComplete('permission_denied');
}
};
PersistentConnection.prototype.removeListen_ = function (pathString, queryId) {
var normalizedPathString = new Path(pathString).toString(); // normalize path.
var listen;
if (this.listens.has(normalizedPathString)) {
var map = this.listens.get(normalizedPathString);
listen = map.get(queryId);
map.delete(queryId);
if (map.size === 0) {
this.listens.delete(normalizedPathString);
}
}
else {
// all listens for this path has already been removed
listen = undefined;
}
return listen;
};
PersistentConnection.prototype.onAuthRevoked_ = function (statusCode, explanation) {
log('Auth token revoked: ' + statusCode + '/' + explanation);
this.authToken_ = null;
this.forceTokenRefresh_ = true;
this.realtime_.close();
if (statusCode === 'invalid_token' || statusCode === 'permission_denied') {
// We'll wait a couple times before logging the warning / increasing the
// retry period since oauth tokens will report as "invalid" if they're
// just expired. Plus there may be transient issues that resolve themselves.
this.invalidAuthTokenCount_++;
if (this.invalidAuthTokenCount_ >= INVALID_AUTH_TOKEN_THRESHOLD) {
// Set a long reconnect delay because recovery is unlikely
this.reconnectDelay_ = RECONNECT_MAX_DELAY_FOR_ADMINS;
// Notify the auth token provider that the token is invalid, which will log
// a warning
this.authTokenProvider_.notifyForInvalidToken();
}
}
};
PersistentConnection.prototype.onSecurityDebugPacket_ = function (body) {
if (this.securityDebugCallback_) {
this.securityDebugCallback_(body);
}
else {
if ('msg' in body) {
console.log('FIREBASE: ' + body['msg'].replace('\n', '\nFIREBASE: '));
}
}
};
PersistentConnection.prototype.restoreState_ = function () {
var e_1, _a, e_2, _b;
//Re-authenticate ourselves if we have a credential stored.
this.tryAuth();
try {
// Puts depend on having received the corresponding data update from the server before they complete, so we must
// make sure to send listens before puts.
for (var _c = __values(this.listens.values()), _d = _c.next(); !_d.done; _d = _c.next()) {
var queries = _d.value;
try {
for (var _e = (e_2 = void 0, __values(queries.values())), _f = _e.next(); !_f.done; _f = _e.next()) {
var listenSpec = _f.value;
this.sendListen_(listenSpec);
}
}
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; }
}
for (var i = 0; i < this.outstandingPuts_.length; i++) {
if (this.outstandingPuts_[i]) {
this.sendPut_(i);
}
}
while (this.onDisconnectRequestQueue_.length) {
var request = this.onDisconnectRequestQueue_.shift();
this.sendOnDisconnect_(request.action, request.pathString, request.data, request.onComplete);
}
for (var i = 0; i < this.outstandingGets_.length; i++) {
if (this.outstandingGets_[i]) {
this.sendGet_(i);
}
}
};
/**
* Sends client stats for first connection
*/
PersistentConnection.prototype.sendConnectStats_ = function () {
var stats = {};
var clientName = 'js';
stats['sdk.' + clientName + '.' + SDK_VERSION.replace(/\./g, '-')] = 1;
if (isMobileCordova()) {
stats['framework.cordova'] = 1;
}
else if (isReactNative()) {
stats['framework.reactnative'] = 1;
}
this.reportStats(stats);
};
PersistentConnection.prototype.shouldReconnect_ = function () {
var online = OnlineMonitor.getInstance().currentlyOnline();
return isEmpty(this.interruptReasons_) && online;
};
PersistentConnection.nextPersistentConnectionId_ = 0;
/**
* Counter for number of connections created. Mainly used for tagging in the logs
*/
PersistentConnection.nextConnectionId_ = 0;
return PersistentConnection;
}(ServerActions));
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* An implementation of ServerActions that communicates with the server via REST requests.
* This is mostly useful for compatibility with crawlers, where we don't want to spin up a full
* persistent connection (using WebSockets or long-polling)
*/
var ReadonlyRestClient = /** @class */ (function (_super) {
__extends$1(ReadonlyRestClient, _super);
/**
* @param {!RepoInfo} repoInfo_ Data about the namespace we are connecting to
* @param {function(string, *, boolean, ?number)} onDataUpdate_ A callback for new data from the server
* @param {AuthTokenProvider} authTokenProvider_
* @implements {ServerActions}
*/
function ReadonlyRestClient(repoInfo_, onDataUpdate_, authTokenProvider_) {
var _this = _super.call(this) || this;
_this.repoInfo_ = repoInfo_;
_this.onDataUpdate_ = onDataUpdate_;
_this.authTokenProvider_ = authTokenProvider_;
/** @private {function(...[*])} */
_this.log_ = logWrapper('p:rest:');
/**
* We don't actually need to track listens, except to prevent us calling an onComplete for a listen
* that's been removed. :-/
*
* @private {!Object.}
*/
_this.listens_ = {};
return _this;
}
ReadonlyRestClient.prototype.reportStats = function (stats) {
throw new Error('Method not implemented.');
};
/**
* @param {!Query} query
* @param {?number=} tag
* @return {string}
* @private
*/
ReadonlyRestClient.getListenId_ = function (query, tag) {
if (tag !== undefined) {
return 'tag$' + tag;
}
else {
assert(query.getQueryParams().isDefault(), "should have a tag if it's not a default query.");
return query.path.toString();
}
};
/** @inheritDoc */
ReadonlyRestClient.prototype.listen = function (query, currentHashFn, tag, onComplete) {
var _this = this;
var pathString = query.path.toString();
this.log_('Listen called for ' + pathString + ' ' + query.queryIdentifier());
// Mark this listener so we can tell if it's removed.
var listenId = ReadonlyRestClient.getListenId_(query, tag);
var thisListen = {};
this.listens_[listenId] = thisListen;
var queryStringParameters = query
.getQueryParams()
.toRestQueryStringParameters();
this.restRequest_(pathString + '.json', queryStringParameters, function (error, result) {
var data = result;
if (error === 404) {
data = null;
error = null;
}
if (error === null) {
_this.onDataUpdate_(pathString, data, /*isMerge=*/ false, tag);
}
if (safeGet(_this.listens_, listenId) === thisListen) {
var status_1;
if (!error) {
status_1 = 'ok';
}
else if (error === 401) {
status_1 = 'permission_denied';
}
else {
status_1 = 'rest_error:' + error;
}
onComplete(status_1, null);
}
});
};
/** @inheritDoc */
ReadonlyRestClient.prototype.unlisten = function (query, tag) {
var listenId = ReadonlyRestClient.getListenId_(query, tag);
delete this.listens_[listenId];
};
ReadonlyRestClient.prototype.get = function (query) {
var _this = this;
var queryStringParameters = query
.getQueryParams()
.toRestQueryStringParameters();
var pathString = query.path.toString();
var deferred = new Deferred();
this.restRequest_(pathString + '.json', queryStringParameters, function (error, result) {
var data = result;
if (error === 404) {
data = null;
error = null;
}
if (error === null) {
_this.onDataUpdate_(pathString, data,
/*isMerge=*/ false,
/*tag=*/ null);
deferred.resolve(data);
}
else {
deferred.reject(new Error(data));
}
});
return deferred.promise;
};
/** @inheritDoc */
ReadonlyRestClient.prototype.refreshAuthToken = function (token) {
// no-op since we just always call getToken.
};
/**
* Performs a REST request to the given path, with the provided query string parameters,
* and any auth credentials we have.
*
* @param {!string} pathString
* @param {!Object.} queryStringParameters
* @param {?function(?number, *=)} callback
* @private
*/
ReadonlyRestClient.prototype.restRequest_ = function (pathString, queryStringParameters, callback) {
var _this = this;
if (queryStringParameters === void 0) { queryStringParameters = {}; }
queryStringParameters['format'] = 'export';
this.authTokenProvider_
.getToken(/*forceRefresh=*/ false)
.then(function (authTokenData) {
var authToken = authTokenData && authTokenData.accessToken;
if (authToken) {
queryStringParameters['auth'] = authToken;
}
var url = (_this.repoInfo_.secure ? 'https://' : 'http://') +
_this.repoInfo_.host +
pathString +
'?' +
'ns=' +
_this.repoInfo_.namespace +
querystring(queryStringParameters);
_this.log_('Sending REST request for ' + url);
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (callback && xhr.readyState === 4) {
_this.log_('REST Response for ' + url + ' received. status:', xhr.status, 'response:', xhr.responseText);
var res = null;
if (xhr.status >= 200 && xhr.status < 300) {
try {
res = jsonEval(xhr.responseText);
}
catch (e) {
warn('Failed to parse JSON response for ' +
url +
': ' +
xhr.responseText);
}
callback(null, res);
}
else {
// 401 and 404 are expected.
if (xhr.status !== 401 && xhr.status !== 404) {
warn('Got unsuccessful REST response for ' +
url +
' Status: ' +
xhr.status);
}
callback(xhr.status);
}
callback = null;
}
};
xhr.open('GET', url, /*asynchronous=*/ true);
xhr.send();
});
};
return ReadonlyRestClient;
}(ServerActions));
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A class that holds metadata about a Repo object
*
* @constructor
*/
var RepoInfo = /** @class */ (function () {
/**
* @param host Hostname portion of the url for the repo
* @param secure Whether or not this repo is accessed over ssl
* @param namespace The namespace represented by the repo
* @param webSocketOnly Whether to prefer websockets over all other transports (used by Nest).
* @param nodeAdmin Whether this instance uses Admin SDK credentials
* @param persistenceKey Override the default session persistence storage key
*/
function RepoInfo(host, secure, namespace, webSocketOnly, nodeAdmin, persistenceKey, includeNamespaceInQueryParams) {
if (nodeAdmin === void 0) { nodeAdmin = false; }
if (persistenceKey === void 0) { persistenceKey = ''; }
if (includeNamespaceInQueryParams === void 0) { includeNamespaceInQueryParams = false; }
this.secure = secure;
this.namespace = namespace;
this.webSocketOnly = webSocketOnly;
this.nodeAdmin = nodeAdmin;
this.persistenceKey = persistenceKey;
this.includeNamespaceInQueryParams = includeNamespaceInQueryParams;
this.host = host.toLowerCase();
this.domain = this.host.substr(this.host.indexOf('.') + 1);
this.internalHost =
PersistentStorage.get('host:' + host) || this.host;
}
RepoInfo.prototype.needsQueryParam = function () {
return (this.host !== this.internalHost ||
this.isCustomHost() ||
this.includeNamespaceInQueryParams);
};
RepoInfo.prototype.isCacheableHost = function () {
return this.internalHost.substr(0, 2) === 's-';
};
RepoInfo.prototype.isDemoHost = function () {
return this.domain === 'firebaseio-demo.com';
};
RepoInfo.prototype.isCustomHost = function () {
return (this.domain !== 'firebaseio.com' && this.domain !== 'firebaseio-demo.com');
};
RepoInfo.prototype.updateHost = function (newHost) {
if (newHost !== this.internalHost) {
this.internalHost = newHost;
if (this.isCacheableHost()) {
PersistentStorage.set('host:' + this.host, this.internalHost);
}
}
};
/**
* Returns the websocket URL for this repo
* @param {string} type of connection
* @param {Object} params list
* @return {string} The URL for this repo
*/
RepoInfo.prototype.connectionURL = function (type, params) {
assert(typeof type === 'string', 'typeof type must == string');
assert(typeof params === 'object', 'typeof params must == object');
var connURL;
if (type === WEBSOCKET) {
connURL =
(this.secure ? 'wss://' : 'ws://') + this.internalHost + '/.ws?';
}
else if (type === LONG_POLLING) {
connURL =
(this.secure ? 'https://' : 'http://') + this.internalHost + '/.lp?';
}
else {
throw new Error('Unknown connection type: ' + type);
}
if (this.needsQueryParam()) {
params['ns'] = this.namespace;
}
var pairs = [];
each(params, function (key, value) {
pairs.push(key + '=' + value);
});
return connURL + pairs.join('&');
};
/** @return {string} */
RepoInfo.prototype.toString = function () {
var str = this.toURLString();
if (this.persistenceKey) {
str += '<' + this.persistenceKey + '>';
}
return str;
};
/** @return {string} */
RepoInfo.prototype.toURLString = function () {
var protocol = this.secure ? 'https://' : 'http://';
var query = this.includeNamespaceInQueryParams
? "?ns=" + this.namespace
: '';
return "" + protocol + this.host + "/" + query;
};
return RepoInfo;
}());
/**
* @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.
*/
/**
* @param {!string} pathString
* @return {string}
*/
function decodePath(pathString) {
var pathStringDecoded = '';
var pieces = pathString.split('/');
for (var i = 0; i < pieces.length; i++) {
if (pieces[i].length > 0) {
var piece = pieces[i];
try {
piece = decodeURIComponent(piece.replace(/\+/g, ' '));
}
catch (e) { }
pathStringDecoded += '/' + piece;
}
}
return pathStringDecoded;
}
/**
* @param {!string} queryString
* @return {!{[key:string]:string}} key value hash
*/
function decodeQuery(queryString) {
var e_1, _a;
var results = {};
if (queryString.charAt(0) === '?') {
queryString = queryString.substring(1);
}
try {
for (var _b = __values(queryString.split('&')), _c = _b.next(); !_c.done; _c = _b.next()) {
var segment = _c.value;
if (segment.length === 0) {
continue;
}
var kv = segment.split('=');
if (kv.length === 2) {
results[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
}
else {
warn("Invalid query segment '" + segment + "' in query '" + queryString + "'");
}
}
}
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 results;
}
var parseRepoInfo = function (dataURL, nodeAdmin) {
var parsedUrl = parseDatabaseURL(dataURL), namespace = parsedUrl.namespace;
if (parsedUrl.domain === 'firebase.com') {
fatal(parsedUrl.host +
' is no longer supported. ' +
'Please use .firebaseio.com instead');
}
// Catch common error of uninitialized namespace value.
if ((!namespace || namespace === 'undefined') &&
parsedUrl.domain !== 'localhost') {
fatal('Cannot parse Firebase url. Please use https://.firebaseio.com');
}
if (!parsedUrl.secure) {
warnIfPageIsSecure();
}
var webSocketOnly = parsedUrl.scheme === 'ws' || parsedUrl.scheme === 'wss';
return {
repoInfo: new RepoInfo(parsedUrl.host, parsedUrl.secure, namespace, nodeAdmin, webSocketOnly,
/*persistenceKey=*/ '',
/*includeNamespaceInQueryParams=*/ namespace !== parsedUrl.subdomain),
path: new Path(parsedUrl.pathString)
};
};
/**
*
* @param {!string} dataURL
* @return {{host: string, port: number, domain: string, subdomain: string, secure: boolean, scheme: string, pathString: string, namespace: string}}
*/
var parseDatabaseURL = function (dataURL) {
// Default to empty strings in the event of a malformed string.
var host = '', domain = '', subdomain = '', pathString = '', namespace = '';
// Always default to SSL, unless otherwise specified.
var secure = true, scheme = 'https', port = 443;
// Don't do any validation here. The caller is responsible for validating the result of parsing.
if (typeof dataURL === 'string') {
// Parse scheme.
var colonInd = dataURL.indexOf('//');
if (colonInd >= 0) {
scheme = dataURL.substring(0, colonInd - 1);
dataURL = dataURL.substring(colonInd + 2);
}
// Parse host, path, and query string.
var slashInd = dataURL.indexOf('/');
if (slashInd === -1) {
slashInd = dataURL.length;
}
var questionMarkInd = dataURL.indexOf('?');
if (questionMarkInd === -1) {
questionMarkInd = dataURL.length;
}
host = dataURL.substring(0, Math.min(slashInd, questionMarkInd));
if (slashInd < questionMarkInd) {
// For pathString, questionMarkInd will always come after slashInd
pathString = decodePath(dataURL.substring(slashInd, questionMarkInd));
}
var queryParams = decodeQuery(dataURL.substring(Math.min(dataURL.length, questionMarkInd)));
// If we have a port, use scheme for determining if it's secure.
colonInd = host.indexOf(':');
if (colonInd >= 0) {
secure = scheme === 'https' || scheme === 'wss';
port = parseInt(host.substring(colonInd + 1), 10);
}
else {
colonInd = host.length;
}
var hostWithoutPort = host.slice(0, colonInd);
if (hostWithoutPort.toLowerCase() === 'localhost') {
domain = 'localhost';
}
else if (hostWithoutPort.split('.').length <= 2) {
domain = hostWithoutPort;
}
else {
// Interpret the subdomain of a 3 or more component URL as the namespace name.
var dotInd = host.indexOf('.');
subdomain = host.substring(0, dotInd).toLowerCase();
domain = host.substring(dotInd + 1);
// Normalize namespaces to lowercase to share storage / connection.
namespace = subdomain;
}
// Always treat the value of the `ns` as the namespace name if it is present.
if ('ns' in queryParams) {
namespace = queryParams['ns'];
}
}
return {
host: host,
port: port,
domain: domain,
subdomain: subdomain,
secure: secure,
scheme: scheme,
pathString: pathString,
namespace: namespace
};
};
/**
* @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.
*/
/**
* True for invalid Firebase keys
* @type {RegExp}
* @private
*/
var INVALID_KEY_REGEX_ = /[\[\].#$\/\u0000-\u001F\u007F]/;
/**
* True for invalid Firebase paths.
* Allows '/' in paths.
* @type {RegExp}
* @private
*/
var INVALID_PATH_REGEX_ = /[\[\].#$\u0000-\u001F\u007F]/;
/**
* Maximum number of characters to allow in leaf value
* @type {number}
* @private
*/
var MAX_LEAF_SIZE_ = 10 * 1024 * 1024;
/**
* @param {*} key
* @return {boolean}
*/
var isValidKey$1 = function (key) {
return (typeof key === 'string' && key.length !== 0 && !INVALID_KEY_REGEX_.test(key));
};
/**
* @param {string} pathString
* @return {boolean}
*/
var isValidPathString = function (pathString) {
return (typeof pathString === 'string' &&
pathString.length !== 0 &&
!INVALID_PATH_REGEX_.test(pathString));
};
/**
* @param {string} pathString
* @return {boolean}
*/
var isValidRootPathString = function (pathString) {
if (pathString) {
// Allow '/.info/' at the beginning.
pathString = pathString.replace(/^\/*\.info(\/|$)/, '/');
}
return isValidPathString(pathString);
};
/**
* @param {*} priority
* @return {boolean}
*/
var isValidPriority = function (priority) {
return (priority === null ||
typeof priority === 'string' ||
(typeof priority === 'number' && !isInvalidJSONNumber(priority)) ||
(priority &&
typeof priority === 'object' &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
contains(priority, '.sv')));
};
/**
* Pre-validate a datum passed as an argument to Firebase function.
*
* @param {string} fnName
* @param {number} argumentNumber
* @param {*} data
* @param {!Path} path
* @param {boolean} optional
*/
var validateFirebaseDataArg = function (fnName, argumentNumber, data, path, optional) {
if (optional && data === undefined) {
return;
}
validateFirebaseData(errorPrefix(fnName, argumentNumber, optional), data, path);
};
/**
* Validate a data object client-side before sending to server.
*
* @param {string} errorPrefix
* @param {*} data
* @param {!Path|!ValidationPath} path_
*/
var validateFirebaseData = function (errorPrefix, data, path_) {
var path = path_ instanceof Path ? new ValidationPath(path_, errorPrefix) : path_;
if (data === undefined) {
throw new Error(errorPrefix + 'contains undefined ' + path.toErrorString());
}
if (typeof data === 'function') {
throw new Error(errorPrefix +
'contains a function ' +
path.toErrorString() +
' with contents = ' +
data.toString());
}
if (isInvalidJSONNumber(data)) {
throw new Error(errorPrefix + 'contains ' + data.toString() + ' ' + path.toErrorString());
}
// Check max leaf size, but try to avoid the utf8 conversion if we can.
if (typeof data === 'string' &&
data.length > MAX_LEAF_SIZE_ / 3 &&
stringLength(data) > MAX_LEAF_SIZE_) {
throw new Error(errorPrefix +
'contains a string greater than ' +
MAX_LEAF_SIZE_ +
' utf8 bytes ' +
path.toErrorString() +
" ('" +
data.substring(0, 50) +
"...')");
}
// TODO = Perf = Consider combining the recursive validation of keys into NodeFromJSON
// to save extra walking of large objects.
if (data && typeof data === 'object') {
var hasDotValue_1 = false;
var hasActualChild_1 = false;
each(data, function (key, value) {
if (key === '.value') {
hasDotValue_1 = true;
}
else if (key !== '.priority' && key !== '.sv') {
hasActualChild_1 = true;
if (!isValidKey$1(key)) {
throw new Error(errorPrefix +
' contains an invalid key (' +
key +
') ' +
path.toErrorString() +
'. Keys must be non-empty strings ' +
'and can\'t contain ".", "#", "$", "/", "[", or "]"');
}
}
path.push(key);
validateFirebaseData(errorPrefix, value, path);
path.pop();
});
if (hasDotValue_1 && hasActualChild_1) {
throw new Error(errorPrefix +
' contains ".value" child ' +
path.toErrorString() +
' in addition to actual children.');
}
}
};
/**
* Pre-validate paths passed in the firebase function.
*
* @param {string} errorPrefix
* @param {Array} mergePaths
*/
var validateFirebaseMergePaths = function (errorPrefix, mergePaths) {
var i, curPath;
for (i = 0; i < mergePaths.length; i++) {
curPath = mergePaths[i];
var keys = curPath.slice();
for (var j = 0; j < keys.length; j++) {
if (keys[j] === '.priority' && j === keys.length - 1) ;
else if (!isValidKey$1(keys[j])) {
throw new Error(errorPrefix +
'contains an invalid key (' +
keys[j] +
') in path ' +
curPath.toString() +
'. Keys must be non-empty strings ' +
'and can\'t contain ".", "#", "$", "/", "[", or "]"');
}
}
}
// Check that update keys are not descendants of each other.
// We rely on the property that sorting guarantees that ancestors come
// right before descendants.
mergePaths.sort(Path.comparePaths);
var prevPath = null;
for (i = 0; i < mergePaths.length; i++) {
curPath = mergePaths[i];
if (prevPath !== null && prevPath.contains(curPath)) {
throw new Error(errorPrefix +
'contains a path ' +
prevPath.toString() +
' that is ancestor of another path ' +
curPath.toString());
}
prevPath = curPath;
}
};
/**
* pre-validate an object passed as an argument to firebase function (
* must be an object - e.g. for firebase.update()).
*
* @param {string} fnName
* @param {number} argumentNumber
* @param {*} data
* @param {!Path} path
* @param {boolean} optional
*/
var validateFirebaseMergeDataArg = function (fnName, argumentNumber, data, path, optional) {
if (optional && data === undefined) {
return;
}
var errorPrefix$1 = errorPrefix(fnName, argumentNumber, optional);
if (!(data && typeof data === 'object') || Array.isArray(data)) {
throw new Error(errorPrefix$1 + ' must be an object containing the children to replace.');
}
var mergePaths = [];
each(data, function (key, value) {
var curPath = new Path(key);
validateFirebaseData(errorPrefix$1, value, path.child(curPath));
if (curPath.getBack() === '.priority') {
if (!isValidPriority(value)) {
throw new Error(errorPrefix$1 +
"contains an invalid value for '" +
curPath.toString() +
"', which must be a valid " +
'Firebase priority (a string, finite number, server value, or null).');
}
}
mergePaths.push(curPath);
});
validateFirebaseMergePaths(errorPrefix$1, mergePaths);
};
var validatePriority = function (fnName, argumentNumber, priority, optional) {
if (optional && priority === undefined) {
return;
}
if (isInvalidJSONNumber(priority)) {
throw new Error(errorPrefix(fnName, argumentNumber, optional) +
'is ' +
priority.toString() +
', but must be a valid Firebase priority (a string, finite number, ' +
'server value, or null).');
}
// Special case to allow importing data with a .sv.
if (!isValidPriority(priority)) {
throw new Error(errorPrefix(fnName, argumentNumber, optional) +
'must be a valid Firebase priority ' +
'(a string, finite number, server value, or null).');
}
};
var validateEventType = function (fnName, argumentNumber, eventType, optional) {
if (optional && eventType === undefined) {
return;
}
switch (eventType) {
case 'value':
case 'child_added':
case 'child_removed':
case 'child_changed':
case 'child_moved':
break;
default:
throw new Error(errorPrefix(fnName, argumentNumber, optional) +
'must be a valid event type = "value", "child_added", "child_removed", ' +
'"child_changed", or "child_moved".');
}
};
var validateKey = function (fnName, argumentNumber, key, optional) {
if (optional && key === undefined) {
return;
}
if (!isValidKey$1(key)) {
throw new Error(errorPrefix(fnName, argumentNumber, optional) +
'was an invalid key = "' +
key +
'". Firebase keys must be non-empty strings and ' +
'can\'t contain ".", "#", "$", "/", "[", or "]").');
}
};
var validatePathString = function (fnName, argumentNumber, pathString, optional) {
if (optional && pathString === undefined) {
return;
}
if (!isValidPathString(pathString)) {
throw new Error(errorPrefix(fnName, argumentNumber, optional) +
'was an invalid path = "' +
pathString +
'". Paths must be non-empty strings and ' +
'can\'t contain ".", "#", "$", "[", or "]"');
}
};
var validateRootPathString = function (fnName, argumentNumber, pathString, optional) {
if (pathString) {
// Allow '/.info/' at the beginning.
pathString = pathString.replace(/^\/*\.info(\/|$)/, '/');
}
validatePathString(fnName, argumentNumber, pathString, optional);
};
var validateWritablePath = function (fnName, path) {
if (path.getFront() === '.info') {
throw new Error(fnName + " failed = Can't modify data under /.info/");
}
};
var validateUrl = function (fnName, argumentNumber, parsedUrl) {
// TODO = Validate server better.
var pathString = parsedUrl.path.toString();
if (!(typeof parsedUrl.repoInfo.host === 'string') ||
parsedUrl.repoInfo.host.length === 0 ||
(!isValidKey$1(parsedUrl.repoInfo.namespace) &&
parsedUrl.repoInfo.host.split(':')[0] !== 'localhost') ||
(pathString.length !== 0 && !isValidRootPathString(pathString))) {
throw new Error(errorPrefix(fnName, argumentNumber, false) +
'must be a valid firebase URL and ' +
'the path can\'t contain ".", "#", "$", "[", or "]".');
}
};
var validateBoolean = function (fnName, argumentNumber, bool, optional) {
if (optional && bool === undefined) {
return;
}
if (typeof bool !== 'boolean') {
throw new Error(errorPrefix(fnName, argumentNumber, optional) + 'must be a boolean.');
}
};
/**
* @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.
*/
/**
* @constructor
*/
var OnDisconnect = /** @class */ (function () {
/**
* @param {!Repo} repo_
* @param {!Path} path_
*/
function OnDisconnect(repo_, path_) {
this.repo_ = repo_;
this.path_ = path_;
}
/**
* @param {function(?Error)=} onComplete
* @return {!firebase.Promise}
*/
OnDisconnect.prototype.cancel = function (onComplete) {
validateArgCount('OnDisconnect.cancel', 0, 1, arguments.length);
validateCallback('OnDisconnect.cancel', 1, onComplete, true);
var deferred = new Deferred();
this.repo_.onDisconnectCancel(this.path_, deferred.wrapCallback(onComplete));
return deferred.promise;
};
/**
* @param {function(?Error)=} onComplete
* @return {!firebase.Promise}
*/
OnDisconnect.prototype.remove = function (onComplete) {
validateArgCount('OnDisconnect.remove', 0, 1, arguments.length);
validateWritablePath('OnDisconnect.remove', this.path_);
validateCallback('OnDisconnect.remove', 1, onComplete, true);
var deferred = new Deferred();
this.repo_.onDisconnectSet(this.path_, null, deferred.wrapCallback(onComplete));
return deferred.promise;
};
/**
* @param {*} value
* @param {function(?Error)=} onComplete
* @return {!firebase.Promise}
*/
OnDisconnect.prototype.set = function (value, onComplete) {
validateArgCount('OnDisconnect.set', 1, 2, arguments.length);
validateWritablePath('OnDisconnect.set', this.path_);
validateFirebaseDataArg('OnDisconnect.set', 1, value, this.path_, false);
validateCallback('OnDisconnect.set', 2, onComplete, true);
var deferred = new Deferred();
this.repo_.onDisconnectSet(this.path_, value, deferred.wrapCallback(onComplete));
return deferred.promise;
};
/**
* @param {*} value
* @param {number|string|null} priority
* @param {function(?Error)=} onComplete
* @return {!firebase.Promise}
*/
OnDisconnect.prototype.setWithPriority = function (value, priority, onComplete) {
validateArgCount('OnDisconnect.setWithPriority', 2, 3, arguments.length);
validateWritablePath('OnDisconnect.setWithPriority', this.path_);
validateFirebaseDataArg('OnDisconnect.setWithPriority', 1, value, this.path_, false);
validatePriority('OnDisconnect.setWithPriority', 2, priority, false);
validateCallback('OnDisconnect.setWithPriority', 3, onComplete, true);
var deferred = new Deferred();
this.repo_.onDisconnectSetWithPriority(this.path_, value, priority, deferred.wrapCallback(onComplete));
return deferred.promise;
};
/**
* @param {!Object} objectToMerge
* @param {function(?Error)=} onComplete
* @return {!firebase.Promise}
*/
OnDisconnect.prototype.update = function (objectToMerge, onComplete) {
validateArgCount('OnDisconnect.update', 1, 2, arguments.length);
validateWritablePath('OnDisconnect.update', this.path_);
if (Array.isArray(objectToMerge)) {
var newObjectToMerge = {};
for (var i = 0; i < objectToMerge.length; ++i) {
newObjectToMerge['' + i] = objectToMerge[i];
}
objectToMerge = newObjectToMerge;
warn('Passing an Array to firebase.database.onDisconnect().update() is deprecated. Use set() if you want to overwrite the ' +
'existing data, or an Object with integer keys if you really do want to only update some of the children.');
}
validateFirebaseMergeDataArg('OnDisconnect.update', 1, objectToMerge, this.path_, false);
validateCallback('OnDisconnect.update', 2, onComplete, true);
var deferred = new Deferred();
this.repo_.onDisconnectUpdate(this.path_, objectToMerge, deferred.wrapCallback(onComplete));
return deferred.promise;
};
return OnDisconnect;
}());
/**
* @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 TransactionResult = /** @class */ (function () {
/**
* A type for the resolve value of Firebase.transaction.
* @constructor
* @dict
* @param {boolean} committed
* @param {DataSnapshot} snapshot
*/
function TransactionResult(committed, snapshot) {
this.committed = committed;
this.snapshot = snapshot;
}
// Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary
// for end-users
TransactionResult.prototype.toJSON = function () {
validateArgCount('TransactionResult.toJSON', 0, 1, arguments.length);
return { committed: this.committed, snapshot: this.snapshot.toJSON() };
};
return TransactionResult;
}());
/**
* @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.
*/
// Modeled after base64 web-safe chars, but ordered by ASCII.
var PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
var MIN_PUSH_CHAR = '-';
var MAX_PUSH_CHAR = 'z';
var MAX_KEY_LEN = 786;
/**
* Fancy ID generator that creates 20-character string identifiers with the
* following properties:
*
* 1. They're based on timestamp so that they sort *after* any existing ids.
* 2. They contain 72-bits of random data after the timestamp so that IDs won't
* collide with other clients' IDs.
* 3. They sort *lexicographically* (so the timestamp is converted to characters
* that will sort properly).
* 4. They're monotonically increasing. Even if you generate more than one in
* the same timestamp, the latter ones will sort after the former ones. We do
* this by using the previous random bits but "incrementing" them by 1 (only
* in the case of a timestamp collision).
*/
var nextPushId = (function () {
// Timestamp of last push, used to prevent local collisions if you push twice
// in one ms.
var lastPushTime = 0;
// We generate 72-bits of randomness which get turned into 12 characters and
// appended to the timestamp to prevent collisions with other clients. We
// store the last characters we generated because in the event of a collision,
// we'll use those same characters except "incremented" by one.
var lastRandChars = [];
return function (now) {
var duplicateTime = now === lastPushTime;
lastPushTime = now;
var i;
var timeStampChars = new Array(8);
for (i = 7; i >= 0; i--) {
timeStampChars[i] = PUSH_CHARS.charAt(now % 64);
// NOTE: Can't use << here because javascript will convert to int and lose
// the upper bits.
now = Math.floor(now / 64);
}
assert(now === 0, 'Cannot push at time == 0');
var id = timeStampChars.join('');
if (!duplicateTime) {
for (i = 0; i < 12; i++) {
lastRandChars[i] = Math.floor(Math.random() * 64);
}
}
else {
// If the timestamp hasn't changed since last push, use the same random
// number, except incremented by 1.
for (i = 11; i >= 0 && lastRandChars[i] === 63; i--) {
lastRandChars[i] = 0;
}
lastRandChars[i]++;
}
for (i = 0; i < 12; i++) {
id += PUSH_CHARS.charAt(lastRandChars[i]);
}
assert(id.length === 20, 'nextPushId: Length should be 20.');
return id;
};
})();
var successor = function (key) {
if (key === '' + INTEGER_32_MAX) {
// See https://firebase.google.com/docs/database/web/lists-of-data#data-order
return MIN_PUSH_CHAR;
}
var keyAsInt = tryParseInt(key);
if (keyAsInt != null) {
return '' + (keyAsInt + 1);
}
var next = new Array(key.length);
for (var i_1 = 0; i_1 < next.length; i_1++) {
next[i_1] = key.charAt(i_1);
}
if (next.length < MAX_KEY_LEN) {
next.push(MIN_PUSH_CHAR);
return next.join('');
}
var i = next.length - 1;
while (i >= 0 && next[i] === MAX_PUSH_CHAR) {
i--;
}
// `successor` was called on the largest possible key, so return the
// MAX_NAME, which sorts larger than all keys.
if (i === -1) {
return MAX_NAME;
}
var source = next[i];
var sourcePlusOne = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(source) + 1);
next[i] = sourcePlusOne;
return next.slice(0, i + 1).join('');
};
// `key` is assumed to be non-empty.
var predecessor = function (key) {
if (key === '' + INTEGER_32_MIN) {
return MIN_NAME;
}
var keyAsInt = tryParseInt(key);
if (keyAsInt != null) {
return '' + (keyAsInt - 1);
}
var next = new Array(key.length);
for (var i = 0; i < next.length; i++) {
next[i] = key.charAt(i);
}
// If `key` ends in `MIN_PUSH_CHAR`, the largest key lexicographically
// smaller than `key`, is `key[0:key.length - 1]`. The next key smaller
// than that, `predecessor(predecessor(key))`, is
//
// `key[0:key.length - 2] + (key[key.length - 1] - 1) + \
// { MAX_PUSH_CHAR repeated MAX_KEY_LEN - (key.length - 1) times }
//
// analogous to increment/decrement for base-10 integers.
//
// This works because lexigographic comparison works character-by-character,
// using length as a tie-breaker if one key is a prefix of the other.
if (next[next.length - 1] === MIN_PUSH_CHAR) {
if (next.length === 1) {
// See https://firebase.google.com/docs/database/web/lists-of-data#orderbykey
return '' + INTEGER_32_MAX;
}
delete next[next.length - 1];
return next.join('');
}
// Replace the last character with it's immediate predecessor, and
// fill the suffix of the key with MAX_PUSH_CHAR. This is the
// lexicographically largest possible key smaller than `key`.
next[next.length - 1] = PUSH_CHARS.charAt(PUSH_CHARS.indexOf(next[next.length - 1]) - 1);
return next.join('') + MAX_PUSH_CHAR.repeat(MAX_KEY_LEN - next.length);
};
/**
* @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.
*/
/**
* @constructor
* @extends {Index}
* @private
*/
var ValueIndex = /** @class */ (function (_super) {
__extends$1(ValueIndex, _super);
function ValueIndex() {
return _super !== null && _super.apply(this, arguments) || this;
}
/**
* @inheritDoc
*/
ValueIndex.prototype.compare = function (a, b) {
var indexCmp = a.node.compareTo(b.node);
if (indexCmp === 0) {
return nameCompare(a.name, b.name);
}
else {
return indexCmp;
}
};
/**
* @inheritDoc
*/
ValueIndex.prototype.isDefinedOn = function (node) {
return true;
};
/**
* @inheritDoc
*/
ValueIndex.prototype.indexedValueChanged = function (oldNode, newNode) {
return !oldNode.equals(newNode);
};
/**
* @inheritDoc
*/
ValueIndex.prototype.minPost = function () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return NamedNode.MIN;
};
/**
* @inheritDoc
*/
ValueIndex.prototype.maxPost = function () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return NamedNode.MAX;
};
/**
* @param {*} indexValue
* @param {string} name
* @return {!NamedNode}
*/
ValueIndex.prototype.makePost = function (indexValue, name) {
var valueNode = nodeFromJSON$1(indexValue);
return new NamedNode(name, valueNode);
};
/**
* @return {!string} String representation for inclusion in a query spec
*/
ValueIndex.prototype.toString = function () {
return '.value';
};
return ValueIndex;
}(Index));
var VALUE_INDEX = new ValueIndex();
/**
* @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.
*/
/**
* @param {!Path} indexPath
* @constructor
* @extends {Index}
*/
var PathIndex = /** @class */ (function (_super) {
__extends$1(PathIndex, _super);
function PathIndex(indexPath_) {
var _this = _super.call(this) || this;
_this.indexPath_ = indexPath_;
assert(!indexPath_.isEmpty() && indexPath_.getFront() !== '.priority', "Can't create PathIndex with empty path or .priority key");
return _this;
}
/**
* @param {!Node} snap
* @return {!Node}
* @protected
*/
PathIndex.prototype.extractChild = function (snap) {
return snap.getChild(this.indexPath_);
};
/**
* @inheritDoc
*/
PathIndex.prototype.isDefinedOn = function (node) {
return !node.getChild(this.indexPath_).isEmpty();
};
/**
* @inheritDoc
*/
PathIndex.prototype.compare = function (a, b) {
var aChild = this.extractChild(a.node);
var bChild = this.extractChild(b.node);
var indexCmp = aChild.compareTo(bChild);
if (indexCmp === 0) {
return nameCompare(a.name, b.name);
}
else {
return indexCmp;
}
};
/**
* @inheritDoc
*/
PathIndex.prototype.makePost = function (indexValue, name) {
var valueNode = nodeFromJSON$1(indexValue);
var node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, valueNode);
return new NamedNode(name, node);
};
/**
* @inheritDoc
*/
PathIndex.prototype.maxPost = function () {
var node = ChildrenNode.EMPTY_NODE.updateChild(this.indexPath_, MAX_NODE$2);
return new NamedNode(MAX_NAME, node);
};
/**
* @inheritDoc
*/
PathIndex.prototype.toString = function () {
return this.indexPath_.slice().join('/');
};
return PathIndex;
}(Index));
/**
* @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.
*/
/**
* Class representing a firebase data snapshot. It wraps a SnapshotNode and
* surfaces the public methods (val, forEach, etc.) we want to expose.
*/
var DataSnapshot = /** @class */ (function () {
/**
* @param {!Node} node_ A SnapshotNode to wrap.
* @param {!Reference} ref_ The ref of the location this snapshot came from.
* @param {!Index} index_ The iteration order for this snapshot
*/
function DataSnapshot(node_, ref_, index_) {
this.node_ = node_;
this.ref_ = ref_;
this.index_ = index_;
}
/**
* Retrieves the snapshot contents as JSON. Returns null if the snapshot is
* empty.
*
* @return {*} JSON representation of the DataSnapshot contents, or null if empty.
*/
DataSnapshot.prototype.val = function () {
validateArgCount('DataSnapshot.val', 0, 0, arguments.length);
return this.node_.val();
};
/**
* Returns the snapshot contents as JSON, including priorities of node. Suitable for exporting
* the entire node contents.
* @return {*} JSON representation of the DataSnapshot contents, or null if empty.
*/
DataSnapshot.prototype.exportVal = function () {
validateArgCount('DataSnapshot.exportVal', 0, 0, arguments.length);
return this.node_.val(true);
};
// Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary
// for end-users
DataSnapshot.prototype.toJSON = function () {
// Optional spacer argument is unnecessary because we're depending on recursion rather than stringifying the content
validateArgCount('DataSnapshot.toJSON', 0, 1, arguments.length);
return this.exportVal();
};
/**
* Returns whether the snapshot contains a non-null value.
*
* @return {boolean} Whether the snapshot contains a non-null value, or is empty.
*/
DataSnapshot.prototype.exists = function () {
validateArgCount('DataSnapshot.exists', 0, 0, arguments.length);
return !this.node_.isEmpty();
};
/**
* Returns a DataSnapshot of the specified child node's contents.
*
* @param {!string} childPathString Path to a child.
* @return {!DataSnapshot} DataSnapshot for child node.
*/
DataSnapshot.prototype.child = function (childPathString) {
validateArgCount('DataSnapshot.child', 0, 1, arguments.length);
// Ensure the childPath is a string (can be a number)
childPathString = String(childPathString);
validatePathString('DataSnapshot.child', 1, childPathString, false);
var childPath = new Path(childPathString);
var childRef = this.ref_.child(childPath);
return new DataSnapshot(this.node_.getChild(childPath), childRef, PRIORITY_INDEX);
};
/**
* Returns whether the snapshot contains a child at the specified path.
*
* @param {!string} childPathString Path to a child.
* @return {boolean} Whether the child exists.
*/
DataSnapshot.prototype.hasChild = function (childPathString) {
validateArgCount('DataSnapshot.hasChild', 1, 1, arguments.length);
validatePathString('DataSnapshot.hasChild', 1, childPathString, false);
var childPath = new Path(childPathString);
return !this.node_.getChild(childPath).isEmpty();
};
/**
* Returns the priority of the object, or null if no priority was set.
*
* @return {string|number|null} The priority.
*/
DataSnapshot.prototype.getPriority = function () {
validateArgCount('DataSnapshot.getPriority', 0, 0, arguments.length);
// typecast here because we never return deferred values or internal priorities (MAX_PRIORITY)
return this.node_.getPriority().val();
};
/**
* Iterates through child nodes and calls the specified action for each one.
*
* @param {function(!DataSnapshot)} action Callback function to be called
* for each child.
* @return {boolean} True if forEach was canceled by action returning true for
* one of the child nodes.
*/
DataSnapshot.prototype.forEach = function (action) {
var _this = this;
validateArgCount('DataSnapshot.forEach', 1, 1, arguments.length);
validateCallback('DataSnapshot.forEach', 1, action, false);
if (this.node_.isLeafNode()) {
return false;
}
var childrenNode = this.node_;
// Sanitize the return value to a boolean. ChildrenNode.forEachChild has a weird return type...
return !!childrenNode.forEachChild(this.index_, function (key, node) {
return action(new DataSnapshot(node, _this.ref_.child(key), PRIORITY_INDEX));
});
};
/**
* Returns whether this DataSnapshot has children.
* @return {boolean} True if the DataSnapshot contains 1 or more child nodes.
*/
DataSnapshot.prototype.hasChildren = function () {
validateArgCount('DataSnapshot.hasChildren', 0, 0, arguments.length);
if (this.node_.isLeafNode()) {
return false;
}
else {
return !this.node_.isEmpty();
}
};
Object.defineProperty(DataSnapshot.prototype, "key", {
get: function () {
return this.ref_.getKey();
},
enumerable: false,
configurable: true
});
/**
* Returns the number of children for this DataSnapshot.
* @return {number} The number of children that this DataSnapshot contains.
*/
DataSnapshot.prototype.numChildren = function () {
validateArgCount('DataSnapshot.numChildren', 0, 0, arguments.length);
return this.node_.numChildren();
};
/**
* @return {Reference} The Firebase reference for the location this snapshot's data came from.
*/
DataSnapshot.prototype.getRef = function () {
validateArgCount('DataSnapshot.ref', 0, 0, arguments.length);
return this.ref_;
};
Object.defineProperty(DataSnapshot.prototype, "ref", {
get: function () {
return this.getRef();
},
enumerable: false,
configurable: true
});
return DataSnapshot;
}());
/**
* @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.
*/
/**
* Encapsulates the data needed to raise an event
* @implements {Event}
*/
var DataEvent = /** @class */ (function () {
/**
* @param {!string} eventType One of: value, child_added, child_changed, child_moved, child_removed
* @param {!EventRegistration} eventRegistration The function to call to with the event data. User provided
* @param {!DataSnapshot} snapshot The data backing the event
* @param {?string=} prevName Optional, the name of the previous child for child_* events.
*/
function DataEvent(eventType, eventRegistration, snapshot, prevName) {
this.eventType = eventType;
this.eventRegistration = eventRegistration;
this.snapshot = snapshot;
this.prevName = prevName;
}
/**
* @inheritDoc
*/
DataEvent.prototype.getPath = function () {
var ref = this.snapshot.getRef();
if (this.eventType === 'value') {
return ref.path;
}
else {
return ref.getParent().path;
}
};
/**
* @inheritDoc
*/
DataEvent.prototype.getEventType = function () {
return this.eventType;
};
/**
* @inheritDoc
*/
DataEvent.prototype.getEventRunner = function () {
return this.eventRegistration.getEventRunner(this);
};
/**
* @inheritDoc
*/
DataEvent.prototype.toString = function () {
return (this.getPath().toString() +
':' +
this.eventType +
':' +
stringify(this.snapshot.exportVal()));
};
return DataEvent;
}());
var CancelEvent = /** @class */ (function () {
/**
* @param {EventRegistration} eventRegistration
* @param {Error} error
* @param {!Path} path
*/
function CancelEvent(eventRegistration, error, path) {
this.eventRegistration = eventRegistration;
this.error = error;
this.path = path;
}
/**
* @inheritDoc
*/
CancelEvent.prototype.getPath = function () {
return this.path;
};
/**
* @inheritDoc
*/
CancelEvent.prototype.getEventType = function () {
return 'cancel';
};
/**
* @inheritDoc
*/
CancelEvent.prototype.getEventRunner = function () {
return this.eventRegistration.getEventRunner(this);
};
/**
* @inheritDoc
*/
CancelEvent.prototype.toString = function () {
return this.path.toString() + ':cancel';
};
return CancelEvent;
}());
/**
* @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.
*/
/**
* Represents registration for 'value' events.
*/
var ValueEventRegistration = /** @class */ (function () {
/**
* @param {?function(!DataSnapshot)} callback_
* @param {?function(Error)} cancelCallback_
* @param {?Object} context_
*/
function ValueEventRegistration(callback_, cancelCallback_, context_) {
this.callback_ = callback_;
this.cancelCallback_ = cancelCallback_;
this.context_ = context_;
}
/**
* @inheritDoc
*/
ValueEventRegistration.prototype.respondsTo = function (eventType) {
return eventType === 'value';
};
/**
* @inheritDoc
*/
ValueEventRegistration.prototype.createEvent = function (change, query) {
var index = query.getQueryParams().getIndex();
return new DataEvent('value', this, new DataSnapshot(change.snapshotNode, query.getRef(), index));
};
/**
* @inheritDoc
*/
ValueEventRegistration.prototype.getEventRunner = function (eventData) {
var ctx = this.context_;
if (eventData.getEventType() === 'cancel') {
assert(this.cancelCallback_, 'Raising a cancel event on a listener with no cancel callback');
var cancelCB_1 = this.cancelCallback_;
return function () {
// We know that error exists, we checked above that this is a cancel event
cancelCB_1.call(ctx, eventData.error);
};
}
else {
var cb_1 = this.callback_;
return function () {
cb_1.call(ctx, eventData.snapshot);
};
}
};
/**
* @inheritDoc
*/
ValueEventRegistration.prototype.createCancelEvent = function (error, path) {
if (this.cancelCallback_) {
return new CancelEvent(this, error, path);
}
else {
return null;
}
};
/**
* @inheritDoc
*/
ValueEventRegistration.prototype.matches = function (other) {
if (!(other instanceof ValueEventRegistration)) {
return false;
}
else if (!other.callback_ || !this.callback_) {
// If no callback specified, we consider it to match any callback.
return true;
}
else {
return (other.callback_ === this.callback_ && other.context_ === this.context_);
}
};
/**
* @inheritDoc
*/
ValueEventRegistration.prototype.hasAnyCallback = function () {
return this.callback_ !== null;
};
return ValueEventRegistration;
}());
/**
* Represents the registration of 1 or more child_xxx events.
*
* Currently, it is always exactly 1 child_xxx event, but the idea is we might let you
* register a group of callbacks together in the future.
*
* @constructor
* @implements {EventRegistration}
*/
var ChildEventRegistration = /** @class */ (function () {
/**
* @param {?Object.} callbacks_
* @param {?function(Error)} cancelCallback_
* @param {Object=} context_
*/
function ChildEventRegistration(callbacks_, cancelCallback_, context_) {
this.callbacks_ = callbacks_;
this.cancelCallback_ = cancelCallback_;
this.context_ = context_;
}
/**
* @inheritDoc
*/
ChildEventRegistration.prototype.respondsTo = function (eventType) {
var eventToCheck = eventType === 'children_added' ? 'child_added' : eventType;
eventToCheck =
eventToCheck === 'children_removed' ? 'child_removed' : eventToCheck;
return contains(this.callbacks_, eventToCheck);
};
/**
* @inheritDoc
*/
ChildEventRegistration.prototype.createCancelEvent = function (error, path) {
if (this.cancelCallback_) {
return new CancelEvent(this, error, path);
}
else {
return null;
}
};
/**
* @inheritDoc
*/
ChildEventRegistration.prototype.createEvent = function (change, query) {
assert(change.childName != null, 'Child events should have a childName.');
var ref = query.getRef().child(/** @type {!string} */ change.childName);
var index = query.getQueryParams().getIndex();
return new DataEvent(change.type, this, new DataSnapshot(change.snapshotNode, ref, index), change.prevName);
};
/**
* @inheritDoc
*/
ChildEventRegistration.prototype.getEventRunner = function (eventData) {
var ctx = this.context_;
if (eventData.getEventType() === 'cancel') {
assert(this.cancelCallback_, 'Raising a cancel event on a listener with no cancel callback');
var cancelCB_2 = this.cancelCallback_;
return function () {
// We know that error exists, we checked above that this is a cancel event
cancelCB_2.call(ctx, eventData.error);
};
}
else {
var cb_2 = this.callbacks_[eventData.eventType];
return function () {
cb_2.call(ctx, eventData.snapshot, eventData.prevName);
};
}
};
/**
* @inheritDoc
*/
ChildEventRegistration.prototype.matches = function (other) {
var _this = this;
if (other instanceof ChildEventRegistration) {
if (!this.callbacks_ || !other.callbacks_) {
return true;
}
else if (this.context_ === other.context_) {
var otherKeys = Object.keys(other.callbacks_);
var thisKeys = Object.keys(this.callbacks_);
var otherCount = otherKeys.length;
var thisCount = thisKeys.length;
if (otherCount === thisCount) {
// If count is 1, do an exact match on eventType, if either is defined but null, it's a match.
// If event types don't match, not a match
// If count is not 1, exact match across all
if (otherCount === 1) {
var otherKey = otherKeys[0];
var thisKey = thisKeys[0];
return (thisKey === otherKey &&
(!other.callbacks_[otherKey] ||
!this.callbacks_[thisKey] ||
other.callbacks_[otherKey] === this.callbacks_[thisKey]));
}
else {
// Exact match on each key.
return thisKeys.every(function (eventType) {
return other.callbacks_[eventType] === _this.callbacks_[eventType];
});
}
}
}
}
return false;
};
/**
* @inheritDoc
*/
ChildEventRegistration.prototype.hasAnyCallback = function () {
return this.callbacks_ !== null;
};
return ChildEventRegistration;
}());
/**
* @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 __referenceConstructor$1;
/**
* A Query represents a filter to be applied to a firebase location. This object purely represents the
* query expression (and exposes our public API to build the query). The actual query logic is in ViewBase.js.
*
* Since every Firebase reference is a query, Firebase inherits from this object.
*/
var Query = /** @class */ (function () {
function Query(repo, path, queryParams_, orderByCalled_) {
this.repo = repo;
this.path = path;
this.queryParams_ = queryParams_;
this.orderByCalled_ = orderByCalled_;
}
Object.defineProperty(Query, "__referenceConstructor", {
get: function () {
assert(__referenceConstructor$1, 'Reference.ts has not been loaded');
return __referenceConstructor$1;
},
set: function (val) {
__referenceConstructor$1 = val;
},
enumerable: false,
configurable: true
});
/**
* Validates start/end values for queries.
* @param {!QueryParams} params
* @private
*/
Query.validateQueryEndpoints_ = function (params) {
var startNode = null;
var endNode = null;
if (params.hasStart()) {
startNode = params.getIndexStartValue();
}
if (params.hasEnd()) {
endNode = params.getIndexEndValue();
}
if (params.getIndex() === KEY_INDEX) {
var tooManyArgsError = 'Query: When ordering by key, you may only pass one argument to ' +
'startAt(), endAt(), or equalTo().';
var wrongArgTypeError = 'Query: When ordering by key, the argument passed to startAt(), startAfter(), ' +
'endAt(), endBefore(), or equalTo() must be a string.';
if (params.hasStart()) {
var startName = params.getIndexStartName();
if (startName !== MIN_NAME) {
throw new Error(tooManyArgsError);
}
else if (typeof startNode !== 'string') {
throw new Error(wrongArgTypeError);
}
}
if (params.hasEnd()) {
var endName = params.getIndexEndName();
if (endName !== MAX_NAME) {
throw new Error(tooManyArgsError);
}
else if (typeof endNode !== 'string') {
throw new Error(wrongArgTypeError);
}
}
}
else if (params.getIndex() === PRIORITY_INDEX) {
if ((startNode != null && !isValidPriority(startNode)) ||
(endNode != null && !isValidPriority(endNode))) {
throw new Error('Query: When ordering by priority, the first argument passed to startAt(), ' +
'startAfter() endAt(), endBefore(), or equalTo() must be a valid priority value ' +
'(null, a number, or a string).');
}
}
else {
assert(params.getIndex() instanceof PathIndex ||
params.getIndex() === VALUE_INDEX, 'unknown index type.');
if ((startNode != null && typeof startNode === 'object') ||
(endNode != null && typeof endNode === 'object')) {
throw new Error('Query: First argument passed to startAt(), startAfter(), endAt(), endBefore(), or ' +
'equalTo() cannot be an object.');
}
}
};
/**
* Validates that limit* has been called with the correct combination of parameters
* @param {!QueryParams} params
* @private
*/
Query.validateLimit_ = function (params) {
if (params.hasStart() &&
params.hasEnd() &&
params.hasLimit() &&
!params.hasAnchoredLimit()) {
throw new Error("Query: Can't combine startAt(), startAfter(), endAt(), endBefore(), and limit(). Use " +
'limitToFirst() or limitToLast() instead.');
}
};
/**
* Validates that no other order by call has been made
* @param {!string} fnName
* @private
*/
Query.prototype.validateNoPreviousOrderByCall_ = function (fnName) {
if (this.orderByCalled_ === true) {
throw new Error(fnName + ": You can't combine multiple orderBy calls.");
}
};
/**
* @return {!QueryParams}
*/
Query.prototype.getQueryParams = function () {
return this.queryParams_;
};
/**
* @return {!Reference}
*/
Query.prototype.getRef = function () {
validateArgCount('Query.ref', 0, 0, arguments.length);
// This is a slight hack. We cannot goog.require('fb.api.Firebase'), since Firebase requires fb.api.Query.
// However, we will always export 'Firebase' to the global namespace, so it's guaranteed to exist by the time this
// method gets called.
return new Query.__referenceConstructor(this.repo, this.path);
};
/**
* @param {!string} eventType
* @param {!function(DataSnapshot, string=)} callback
* @param {(function(Error)|Object)=} cancelCallbackOrContext
* @param {Object=} context
* @return {!function(DataSnapshot, string=)}
*/
Query.prototype.on = function (eventType, callback, cancelCallbackOrContext, context) {
validateArgCount('Query.on', 2, 4, arguments.length);
validateEventType('Query.on', 1, eventType, false);
validateCallback('Query.on', 2, callback, false);
var ret = Query.getCancelAndContextArgs_('Query.on', cancelCallbackOrContext, context);
if (eventType === 'value') {
this.onValueEvent(callback, ret.cancel, ret.context);
}
else {
var callbacks = {};
callbacks[eventType] = callback;
this.onChildEvent(callbacks, ret.cancel, ret.context);
}
return callback;
};
/**
* @param {!function(!DataSnapshot)} callback
* @param {?function(Error)} cancelCallback
* @param {?Object} context
* @protected
*/
Query.prototype.onValueEvent = function (callback, cancelCallback, context) {
var container = new ValueEventRegistration(callback, cancelCallback || null, context || null);
this.repo.addEventCallbackForQuery(this, container);
};
/**
* @param {!Object.} callbacks
* @param {?function(Error)} cancelCallback
* @param {?Object} context
* @protected
*/
Query.prototype.onChildEvent = function (callbacks, cancelCallback, context) {
var container = new ChildEventRegistration(callbacks, cancelCallback, context);
this.repo.addEventCallbackForQuery(this, container);
};
/**
* @param {string=} eventType
* @param {(function(!DataSnapshot, ?string=))=} callback
* @param {Object=} context
*/
Query.prototype.off = function (eventType, callback, context) {
validateArgCount('Query.off', 0, 3, arguments.length);
validateEventType('Query.off', 1, eventType, true);
validateCallback('Query.off', 2, callback, true);
validateContextObject('Query.off', 3, context, true);
var container = null;
var callbacks = null;
if (eventType === 'value') {
var valueCallback = callback || null;
container = new ValueEventRegistration(valueCallback, null, context || null);
}
else if (eventType) {
if (callback) {
callbacks = {};
callbacks[eventType] = callback;
}
container = new ChildEventRegistration(callbacks, null, context || null);
}
this.repo.removeEventCallbackForQuery(this, container);
};
/**
* Get the server-value for this query, or return a cached value if not connected.
*/
Query.prototype.get = function () {
return this.repo.getValue(this);
};
/**
* Attaches a listener, waits for the first event, and then removes the listener
* @param {!string} eventType
* @param {!function(!DataSnapshot, string=)} userCallback
* @param failureCallbackOrContext
* @param context
* @return {!firebase.Promise}
*/
Query.prototype.once = function (eventType, userCallback, failureCallbackOrContext, context) {
var _this = this;
validateArgCount('Query.once', 1, 4, arguments.length);
validateEventType('Query.once', 1, eventType, false);
validateCallback('Query.once', 2, userCallback, true);
var ret = Query.getCancelAndContextArgs_('Query.once', failureCallbackOrContext, context);
// TODO: Implement this more efficiently (in particular, use 'get' wire protocol for 'value' event)
// TODO: consider actually wiring the callbacks into the promise. We cannot do this without a breaking change
// because the API currently expects callbacks will be called synchronously if the data is cached, but this is
// against the Promise specification.
var firstCall = true;
var deferred = new Deferred();
// A dummy error handler in case a user wasn't expecting promises
deferred.promise.catch(function () { });
var onceCallback = function (snapshot) {
// NOTE: Even though we unsubscribe, we may get called multiple times if a single action (e.g. set() with JSON)
// triggers multiple events (e.g. child_added or child_changed).
if (firstCall) {
firstCall = false;
_this.off(eventType, onceCallback);
if (userCallback) {
userCallback.bind(ret.context)(snapshot);
}
deferred.resolve(snapshot);
}
};
this.on(eventType, onceCallback,
/*cancel=*/ function (err) {
_this.off(eventType, onceCallback);
if (ret.cancel) {
ret.cancel.bind(ret.context)(err);
}
deferred.reject(err);
});
return deferred.promise;
};
/**
* Set a limit and anchor it to the start of the window.
* @param {!number} limit
* @return {!Query}
*/
Query.prototype.limitToFirst = function (limit) {
validateArgCount('Query.limitToFirst', 1, 1, arguments.length);
if (typeof limit !== 'number' ||
Math.floor(limit) !== limit ||
limit <= 0) {
throw new Error('Query.limitToFirst: First argument must be a positive integer.');
}
if (this.queryParams_.hasLimit()) {
throw new Error('Query.limitToFirst: Limit was already set (by another call to limit, ' +
'limitToFirst, or limitToLast).');
}
return new Query(this.repo, this.path, this.queryParams_.limitToFirst(limit), this.orderByCalled_);
};
/**
* Set a limit and anchor it to the end of the window.
* @param {!number} limit
* @return {!Query}
*/
Query.prototype.limitToLast = function (limit) {
validateArgCount('Query.limitToLast', 1, 1, arguments.length);
if (typeof limit !== 'number' ||
Math.floor(limit) !== limit ||
limit <= 0) {
throw new Error('Query.limitToLast: First argument must be a positive integer.');
}
if (this.queryParams_.hasLimit()) {
throw new Error('Query.limitToLast: Limit was already set (by another call to limit, ' +
'limitToFirst, or limitToLast).');
}
return new Query(this.repo, this.path, this.queryParams_.limitToLast(limit), this.orderByCalled_);
};
/**
* Given a child path, return a new query ordered by the specified grandchild path.
* @param {!string} path
* @return {!Query}
*/
Query.prototype.orderByChild = function (path) {
validateArgCount('Query.orderByChild', 1, 1, arguments.length);
if (path === '$key') {
throw new Error('Query.orderByChild: "$key" is invalid. Use Query.orderByKey() instead.');
}
else if (path === '$priority') {
throw new Error('Query.orderByChild: "$priority" is invalid. Use Query.orderByPriority() instead.');
}
else if (path === '$value') {
throw new Error('Query.orderByChild: "$value" is invalid. Use Query.orderByValue() instead.');
}
validatePathString('Query.orderByChild', 1, path, false);
this.validateNoPreviousOrderByCall_('Query.orderByChild');
var parsedPath = new Path(path);
if (parsedPath.isEmpty()) {
throw new Error('Query.orderByChild: cannot pass in empty path. Use Query.orderByValue() instead.');
}
var index = new PathIndex(parsedPath);
var newParams = this.queryParams_.orderBy(index);
Query.validateQueryEndpoints_(newParams);
return new Query(this.repo, this.path, newParams, /*orderByCalled=*/ true);
};
/**
* Return a new query ordered by the KeyIndex
* @return {!Query}
*/
Query.prototype.orderByKey = function () {
validateArgCount('Query.orderByKey', 0, 0, arguments.length);
this.validateNoPreviousOrderByCall_('Query.orderByKey');
var newParams = this.queryParams_.orderBy(KEY_INDEX);
Query.validateQueryEndpoints_(newParams);
return new Query(this.repo, this.path, newParams, /*orderByCalled=*/ true);
};
/**
* Return a new query ordered by the PriorityIndex
* @return {!Query}
*/
Query.prototype.orderByPriority = function () {
validateArgCount('Query.orderByPriority', 0, 0, arguments.length);
this.validateNoPreviousOrderByCall_('Query.orderByPriority');
var newParams = this.queryParams_.orderBy(PRIORITY_INDEX);
Query.validateQueryEndpoints_(newParams);
return new Query(this.repo, this.path, newParams, /*orderByCalled=*/ true);
};
/**
* Return a new query ordered by the ValueIndex
* @return {!Query}
*/
Query.prototype.orderByValue = function () {
validateArgCount('Query.orderByValue', 0, 0, arguments.length);
this.validateNoPreviousOrderByCall_('Query.orderByValue');
var newParams = this.queryParams_.orderBy(VALUE_INDEX);
Query.validateQueryEndpoints_(newParams);
return new Query(this.repo, this.path, newParams, /*orderByCalled=*/ true);
};
/**
* @param {number|string|boolean|null} value
* @param {?string=} name
* @return {!Query}
*/
Query.prototype.startAt = function (value, name) {
if (value === void 0) { value = null; }
validateArgCount('Query.startAt', 0, 2, arguments.length);
validateFirebaseDataArg('Query.startAt', 1, value, this.path, true);
validateKey('Query.startAt', 2, name, true);
var newParams = this.queryParams_.startAt(value, name);
Query.validateLimit_(newParams);
Query.validateQueryEndpoints_(newParams);
if (this.queryParams_.hasStart()) {
throw new Error('Query.startAt: Starting point was already set (by another call to startAt ' +
'or equalTo).');
}
// Calling with no params tells us to start at the beginning.
if (value === undefined) {
value = null;
name = null;
}
return new Query(this.repo, this.path, newParams, this.orderByCalled_);
};
Query.prototype.startAfter = function (value, name) {
if (value === void 0) { value = null; }
validateArgCount('Query.startAfter', 0, 2, arguments.length);
validateFirebaseDataArg('Query.startAfter', 1, value, this.path, false);
validateKey('Query.startAfter', 2, name, true);
var newParams = this.queryParams_.startAfter(value, name);
Query.validateLimit_(newParams);
Query.validateQueryEndpoints_(newParams);
if (this.queryParams_.hasStart()) {
throw new Error('Query.startAfter: Starting point was already set (by another call to startAt, startAfter ' +
'or equalTo).');
}
return new Query(this.repo, this.path, newParams, this.orderByCalled_);
};
/**
* @param {number|string|boolean|null} value
* @param {?string=} name
* @return {!Query}
*/
Query.prototype.endAt = function (value, name) {
if (value === void 0) { value = null; }
validateArgCount('Query.endAt', 0, 2, arguments.length);
validateFirebaseDataArg('Query.endAt', 1, value, this.path, true);
validateKey('Query.endAt', 2, name, true);
var newParams = this.queryParams_.endAt(value, name);
Query.validateLimit_(newParams);
Query.validateQueryEndpoints_(newParams);
if (this.queryParams_.hasEnd()) {
throw new Error('Query.endAt: Ending point was already set (by another call to endAt, endBefore, or ' +
'equalTo).');
}
return new Query(this.repo, this.path, newParams, this.orderByCalled_);
};
Query.prototype.endBefore = function (value, name) {
if (value === void 0) { value = null; }
validateArgCount('Query.endBefore', 0, 2, arguments.length);
validateFirebaseDataArg('Query.endBefore', 1, value, this.path, false);
validateKey('Query.endBefore', 2, name, true);
var newParams = this.queryParams_.endBefore(value, name);
Query.validateLimit_(newParams);
Query.validateQueryEndpoints_(newParams);
if (this.queryParams_.hasEnd()) {
throw new Error('Query.endBefore: Ending point was already set (by another call to endAt, endBefore, or ' +
'equalTo).');
}
return new Query(this.repo, this.path, newParams, this.orderByCalled_);
};
/**
* Load the selection of children with exactly the specified value, and, optionally,
* the specified name.
* @param {number|string|boolean|null} value
* @param {string=} name
* @return {!Query}
*/
Query.prototype.equalTo = function (value, name) {
validateArgCount('Query.equalTo', 1, 2, arguments.length);
validateFirebaseDataArg('Query.equalTo', 1, value, this.path, false);
validateKey('Query.equalTo', 2, name, true);
if (this.queryParams_.hasStart()) {
throw new Error('Query.equalTo: Starting point was already set (by another call to startAt/startAfter or ' +
'equalTo).');
}
if (this.queryParams_.hasEnd()) {
throw new Error('Query.equalTo: Ending point was already set (by another call to endAt/endBefore or ' +
'equalTo).');
}
return this.startAt(value, name).endAt(value, name);
};
/**
* @return {!string} URL for this location.
*/
Query.prototype.toString = function () {
validateArgCount('Query.toString', 0, 0, arguments.length);
return this.repo.toString() + this.path.toUrlEncodedString();
};
// Do not create public documentation. This is intended to make JSON serialization work but is otherwise unnecessary
// for end-users.
Query.prototype.toJSON = function () {
// An optional spacer argument is unnecessary for a string.
validateArgCount('Query.toJSON', 0, 1, arguments.length);
return this.toString();
};
/**
* An object representation of the query parameters used by this Query.
* @return {!Object}
*/
Query.prototype.queryObject = function () {
return this.queryParams_.getQueryObject();
};
/**
* @return {!string}
*/
Query.prototype.queryIdentifier = function () {
var obj = this.queryObject();
var id = ObjectToUniqueKey(obj);
return id === '{}' ? 'default' : id;
};
/**
* Return true if this query and the provided query are equivalent; otherwise, return false.
* @param {Query} other
* @return {boolean}
*/
Query.prototype.isEqual = function (other) {
validateArgCount('Query.isEqual', 1, 1, arguments.length);
if (!(other instanceof Query)) {
var error = 'Query.isEqual failed: First argument must be an instance of firebase.database.Query.';
throw new Error(error);
}
var sameRepo = this.repo === other.repo;
var samePath = this.path.equals(other.path);
var sameQueryIdentifier = this.queryIdentifier() === other.queryIdentifier();
return sameRepo && samePath && sameQueryIdentifier;
};
/**
* Helper used by .on and .once to extract the context and or cancel arguments.
* @param {!string} fnName The function name (on or once)
* @param {(function(Error)|Object)=} cancelOrContext
* @param {Object=} context
* @return {{cancel: ?function(Error), context: ?Object}}
* @private
*/
Query.getCancelAndContextArgs_ = function (fnName, cancelOrContext, context) {
var ret = { cancel: null, context: null };
if (cancelOrContext && context) {
ret.cancel = cancelOrContext;
validateCallback(fnName, 3, ret.cancel, true);
ret.context = context;
validateContextObject(fnName, 4, ret.context, true);
}
else if (cancelOrContext) {
// we have either a cancel callback or a context.
if (typeof cancelOrContext === 'object' && cancelOrContext !== null) {
// it's a context!
ret.context = cancelOrContext;
}
else if (typeof cancelOrContext === 'function') {
ret.cancel = cancelOrContext;
}
else {
throw new Error(errorPrefix(fnName, 3, true) +
' must either be a cancel callback or a context object.');
}
}
return ret;
};
Object.defineProperty(Query.prototype, "ref", {
get: function () {
return this.getRef();
},
enumerable: false,
configurable: true
});
return Query;
}());
/**
* @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.
*/
/**
* Filters nodes by range and uses an IndexFilter to track any changes after filtering the node
*
* @constructor
* @implements {NodeFilter}
*/
var RangedFilter = /** @class */ (function () {
/**
* @param {!QueryParams} params
*/
function RangedFilter(params) {
this.indexedFilter_ = new IndexedFilter(params.getIndex());
this.index_ = params.getIndex();
this.startPost_ = RangedFilter.getStartPost_(params);
this.endPost_ = RangedFilter.getEndPost_(params);
}
/**
* @return {!NamedNode}
*/
RangedFilter.prototype.getStartPost = function () {
return this.startPost_;
};
/**
* @return {!NamedNode}
*/
RangedFilter.prototype.getEndPost = function () {
return this.endPost_;
};
/**
* @param {!NamedNode} node
* @return {boolean}
*/
RangedFilter.prototype.matches = function (node) {
return (this.index_.compare(this.getStartPost(), node) <= 0 &&
this.index_.compare(node, this.getEndPost()) <= 0);
};
/**
* @inheritDoc
*/
RangedFilter.prototype.updateChild = function (snap, key, newChild, affectedPath, source, optChangeAccumulator) {
if (!this.matches(new NamedNode(key, newChild))) {
newChild = ChildrenNode.EMPTY_NODE;
}
return this.indexedFilter_.updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator);
};
/**
* @inheritDoc
*/
RangedFilter.prototype.updateFullNode = function (oldSnap, newSnap, optChangeAccumulator) {
if (newSnap.isLeafNode()) {
// Make sure we have a children node with the correct index, not a leaf node;
newSnap = ChildrenNode.EMPTY_NODE;
}
var filtered = newSnap.withIndex(this.index_);
// Don't support priorities on queries
filtered = filtered.updatePriority(ChildrenNode.EMPTY_NODE);
var self = this;
newSnap.forEachChild(PRIORITY_INDEX, function (key, childNode) {
if (!self.matches(new NamedNode(key, childNode))) {
filtered = filtered.updateImmediateChild(key, ChildrenNode.EMPTY_NODE);
}
});
return this.indexedFilter_.updateFullNode(oldSnap, filtered, optChangeAccumulator);
};
/**
* @inheritDoc
*/
RangedFilter.prototype.updatePriority = function (oldSnap, newPriority) {
// Don't support priorities on queries
return oldSnap;
};
/**
* @inheritDoc
*/
RangedFilter.prototype.filtersNodes = function () {
return true;
};
/**
* @inheritDoc
*/
RangedFilter.prototype.getIndexedFilter = function () {
return this.indexedFilter_;
};
/**
* @inheritDoc
*/
RangedFilter.prototype.getIndex = function () {
return this.index_;
};
/**
* @param {!QueryParams} params
* @return {!NamedNode}
* @private
*/
RangedFilter.getStartPost_ = function (params) {
if (params.hasStart()) {
var startName = params.getIndexStartName();
return params.getIndex().makePost(params.getIndexStartValue(), startName);
}
else {
return params.getIndex().minPost();
}
};
/**
* @param {!QueryParams} params
* @return {!NamedNode}
* @private
*/
RangedFilter.getEndPost_ = function (params) {
if (params.hasEnd()) {
var endName = params.getIndexEndName();
return params.getIndex().makePost(params.getIndexEndValue(), endName);
}
else {
return params.getIndex().maxPost();
}
};
return RangedFilter;
}());
/**
* @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.
*/
/**
* Applies a limit and a range to a node and uses RangedFilter to do the heavy lifting where possible
*
* @constructor
* @implements {NodeFilter}
*/
var LimitedFilter = /** @class */ (function () {
/**
* @param {!QueryParams} params
*/
function LimitedFilter(params) {
this.rangedFilter_ = new RangedFilter(params);
this.index_ = params.getIndex();
this.limit_ = params.getLimit();
this.reverse_ = !params.isViewFromLeft();
}
/**
* @inheritDoc
*/
LimitedFilter.prototype.updateChild = function (snap, key, newChild, affectedPath, source, optChangeAccumulator) {
if (!this.rangedFilter_.matches(new NamedNode(key, newChild))) {
newChild = ChildrenNode.EMPTY_NODE;
}
if (snap.getImmediateChild(key).equals(newChild)) {
// No change
return snap;
}
else if (snap.numChildren() < this.limit_) {
return this.rangedFilter_
.getIndexedFilter()
.updateChild(snap, key, newChild, affectedPath, source, optChangeAccumulator);
}
else {
return this.fullLimitUpdateChild_(snap, key, newChild, source, optChangeAccumulator);
}
};
/**
* @inheritDoc
*/
LimitedFilter.prototype.updateFullNode = function (oldSnap, newSnap, optChangeAccumulator) {
var filtered;
if (newSnap.isLeafNode() || newSnap.isEmpty()) {
// Make sure we have a children node with the correct index, not a leaf node;
filtered = ChildrenNode.EMPTY_NODE.withIndex(this.index_);
}
else {
if (this.limit_ * 2 < newSnap.numChildren() &&
newSnap.isIndexed(this.index_)) {
// Easier to build up a snapshot, since what we're given has more than twice the elements we want
filtered = ChildrenNode.EMPTY_NODE.withIndex(this.index_);
// anchor to the startPost, endPost, or last element as appropriate
var iterator = void 0;
if (this.reverse_) {
iterator = newSnap.getReverseIteratorFrom(this.rangedFilter_.getEndPost(), this.index_);
}
else {
iterator = newSnap.getIteratorFrom(this.rangedFilter_.getStartPost(), this.index_);
}
var count = 0;
while (iterator.hasNext() && count < this.limit_) {
var next = iterator.getNext();
var inRange = void 0;
if (this.reverse_) {
inRange =
this.index_.compare(this.rangedFilter_.getStartPost(), next) <= 0;
}
else {
inRange =
this.index_.compare(next, this.rangedFilter_.getEndPost()) <= 0;
}
if (inRange) {
filtered = filtered.updateImmediateChild(next.name, next.node);
count++;
}
else {
// if we have reached the end post, we cannot keep adding elemments
break;
}
}
}
else {
// The snap contains less than twice the limit. Faster to delete from the snap than build up a new one
filtered = newSnap.withIndex(this.index_);
// Don't support priorities on queries
filtered = filtered.updatePriority(ChildrenNode.EMPTY_NODE);
var startPost = void 0;
var endPost = void 0;
var cmp = void 0;
var iterator = void 0;
if (this.reverse_) {
iterator = filtered.getReverseIterator(this.index_);
startPost = this.rangedFilter_.getEndPost();
endPost = this.rangedFilter_.getStartPost();
var indexCompare_1 = this.index_.getCompare();
cmp = function (a, b) { return indexCompare_1(b, a); };
}
else {
iterator = filtered.getIterator(this.index_);
startPost = this.rangedFilter_.getStartPost();
endPost = this.rangedFilter_.getEndPost();
cmp = this.index_.getCompare();
}
var count = 0;
var foundStartPost = false;
while (iterator.hasNext()) {
var next = iterator.getNext();
if (!foundStartPost && cmp(startPost, next) <= 0) {
// start adding
foundStartPost = true;
}
var inRange = foundStartPost && count < this.limit_ && cmp(next, endPost) <= 0;
if (inRange) {
count++;
}
else {
filtered = filtered.updateImmediateChild(next.name, ChildrenNode.EMPTY_NODE);
}
}
}
}
return this.rangedFilter_
.getIndexedFilter()
.updateFullNode(oldSnap, filtered, optChangeAccumulator);
};
/**
* @inheritDoc
*/
LimitedFilter.prototype.updatePriority = function (oldSnap, newPriority) {
// Don't support priorities on queries
return oldSnap;
};
/**
* @inheritDoc
*/
LimitedFilter.prototype.filtersNodes = function () {
return true;
};
/**
* @inheritDoc
*/
LimitedFilter.prototype.getIndexedFilter = function () {
return this.rangedFilter_.getIndexedFilter();
};
/**
* @inheritDoc
*/
LimitedFilter.prototype.getIndex = function () {
return this.index_;
};
/**
* @param {!Node} snap
* @param {string} childKey
* @param {!Node} childSnap
* @param {!CompleteChildSource} source
* @param {?ChildChangeAccumulator} changeAccumulator
* @return {!Node}
* @private
*/
LimitedFilter.prototype.fullLimitUpdateChild_ = function (snap, childKey, childSnap, source, changeAccumulator) {
// TODO: rename all cache stuff etc to general snap terminology
var cmp;
if (this.reverse_) {
var indexCmp_1 = this.index_.getCompare();
cmp = function (a, b) { return indexCmp_1(b, a); };
}
else {
cmp = this.index_.getCompare();
}
var oldEventCache = snap;
assert(oldEventCache.numChildren() === this.limit_, '');
var newChildNamedNode = new NamedNode(childKey, childSnap);
var windowBoundary = this.reverse_
? oldEventCache.getFirstChild(this.index_)
: oldEventCache.getLastChild(this.index_);
var inRange = this.rangedFilter_.matches(newChildNamedNode);
if (oldEventCache.hasChild(childKey)) {
var oldChildSnap = oldEventCache.getImmediateChild(childKey);
var nextChild = source.getChildAfterChild(this.index_, windowBoundary, this.reverse_);
while (nextChild != null &&
(nextChild.name === childKey || oldEventCache.hasChild(nextChild.name))) {
// There is a weird edge case where a node is updated as part of a merge in the write tree, but hasn't
// been applied to the limited filter yet. Ignore this next child which will be updated later in
// the limited filter...
nextChild = source.getChildAfterChild(this.index_, nextChild, this.reverse_);
}
var compareNext = nextChild == null ? 1 : cmp(nextChild, newChildNamedNode);
var remainsInWindow = inRange && !childSnap.isEmpty() && compareNext >= 0;
if (remainsInWindow) {
if (changeAccumulator != null) {
changeAccumulator.trackChildChange(Change.childChangedChange(childKey, childSnap, oldChildSnap));
}
return oldEventCache.updateImmediateChild(childKey, childSnap);
}
else {
if (changeAccumulator != null) {
changeAccumulator.trackChildChange(Change.childRemovedChange(childKey, oldChildSnap));
}
var newEventCache = oldEventCache.updateImmediateChild(childKey, ChildrenNode.EMPTY_NODE);
var nextChildInRange = nextChild != null && this.rangedFilter_.matches(nextChild);
if (nextChildInRange) {
if (changeAccumulator != null) {
changeAccumulator.trackChildChange(Change.childAddedChange(nextChild.name, nextChild.node));
}
return newEventCache.updateImmediateChild(nextChild.name, nextChild.node);
}
else {
return newEventCache;
}
}
}
else if (childSnap.isEmpty()) {
// we're deleting a node, but it was not in the window, so ignore it
return snap;
}
else if (inRange) {
if (cmp(windowBoundary, newChildNamedNode) >= 0) {
if (changeAccumulator != null) {
changeAccumulator.trackChildChange(Change.childRemovedChange(windowBoundary.name, windowBoundary.node));
changeAccumulator.trackChildChange(Change.childAddedChange(childKey, childSnap));
}
return oldEventCache
.updateImmediateChild(childKey, childSnap)
.updateImmediateChild(windowBoundary.name, ChildrenNode.EMPTY_NODE);
}
else {
return snap;
}
}
else {
return snap;
}
};
return LimitedFilter;
}());
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This class is an immutable-from-the-public-api struct containing a set of query parameters defining a
* range to be returned for a particular location. It is assumed that validation of parameters is done at the
* user-facing API level, so it is not done here.
* @constructor
*/
var QueryParams = /** @class */ (function () {
function QueryParams() {
this.limitSet_ = false;
this.startSet_ = false;
this.startNameSet_ = false;
this.startAfterSet_ = false;
this.endSet_ = false;
this.endNameSet_ = false;
this.endBeforeSet_ = false;
this.limit_ = 0;
this.viewFrom_ = '';
this.indexStartValue_ = null;
this.indexStartName_ = '';
this.indexEndValue_ = null;
this.indexEndName_ = '';
this.index_ = PRIORITY_INDEX;
}
/**
* @return {boolean}
*/
QueryParams.prototype.hasStart = function () {
return this.startSet_;
};
QueryParams.prototype.hasStartAfter = function () {
return this.startAfterSet_;
};
QueryParams.prototype.hasEndBefore = function () {
return this.endBeforeSet_;
};
/**
* @return {boolean} True if it would return from left.
*/
QueryParams.prototype.isViewFromLeft = function () {
if (this.viewFrom_ === '') {
// limit(), rather than limitToFirst or limitToLast was called.
// This means that only one of startSet_ and endSet_ is true. Use them
// to calculate which side of the view to anchor to. If neither is set,
// anchor to the end.
return this.startSet_;
}
else {
return (this.viewFrom_ === QueryParams.WIRE_PROTOCOL_CONSTANTS_.VIEW_FROM_LEFT);
}
};
/**
* Only valid to call if hasStart() returns true
* @return {*}
*/
QueryParams.prototype.getIndexStartValue = function () {
assert(this.startSet_, 'Only valid if start has been set');
return this.indexStartValue_;
};
/**
* Only valid to call if hasStart() returns true.
* Returns the starting key name for the range defined by these query parameters
* @return {!string}
*/
QueryParams.prototype.getIndexStartName = function () {
assert(this.startSet_, 'Only valid if start has been set');
if (this.startNameSet_) {
return this.indexStartName_;
}
else {
return MIN_NAME;
}
};
/**
* @return {boolean}
*/
QueryParams.prototype.hasEnd = function () {
return this.endSet_;
};
/**
* Only valid to call if hasEnd() returns true.
* @return {*}
*/
QueryParams.prototype.getIndexEndValue = function () {
assert(this.endSet_, 'Only valid if end has been set');
return this.indexEndValue_;
};
/**
* Only valid to call if hasEnd() returns true.
* Returns the end key name for the range defined by these query parameters
* @return {!string}
*/
QueryParams.prototype.getIndexEndName = function () {
assert(this.endSet_, 'Only valid if end has been set');
if (this.endNameSet_) {
return this.indexEndName_;
}
else {
return MAX_NAME;
}
};
/**
* @return {boolean}
*/
QueryParams.prototype.hasLimit = function () {
return this.limitSet_;
};
/**
* @return {boolean} True if a limit has been set and it has been explicitly anchored
*/
QueryParams.prototype.hasAnchoredLimit = function () {
return this.limitSet_ && this.viewFrom_ !== '';
};
/**
* Only valid to call if hasLimit() returns true
* @return {!number}
*/
QueryParams.prototype.getLimit = function () {
assert(this.limitSet_, 'Only valid if limit has been set');
return this.limit_;
};
/**
* @return {!Index}
*/
QueryParams.prototype.getIndex = function () {
return this.index_;
};
/**
* @return {!QueryParams}
* @private
*/
QueryParams.prototype.copy_ = function () {
var copy = new QueryParams();
copy.limitSet_ = this.limitSet_;
copy.limit_ = this.limit_;
copy.startSet_ = this.startSet_;
copy.indexStartValue_ = this.indexStartValue_;
copy.startNameSet_ = this.startNameSet_;
copy.indexStartName_ = this.indexStartName_;
copy.endSet_ = this.endSet_;
copy.indexEndValue_ = this.indexEndValue_;
copy.endNameSet_ = this.endNameSet_;
copy.indexEndName_ = this.indexEndName_;
copy.index_ = this.index_;
copy.viewFrom_ = this.viewFrom_;
return copy;
};
/**
* @param {!number} newLimit
* @return {!QueryParams}
*/
QueryParams.prototype.limit = function (newLimit) {
var newParams = this.copy_();
newParams.limitSet_ = true;
newParams.limit_ = newLimit;
newParams.viewFrom_ = '';
return newParams;
};
/**
* @param {!number} newLimit
* @return {!QueryParams}
*/
QueryParams.prototype.limitToFirst = function (newLimit) {
var newParams = this.copy_();
newParams.limitSet_ = true;
newParams.limit_ = newLimit;
newParams.viewFrom_ = QueryParams.WIRE_PROTOCOL_CONSTANTS_.VIEW_FROM_LEFT;
return newParams;
};
/**
* @param {!number} newLimit
* @return {!QueryParams}
*/
QueryParams.prototype.limitToLast = function (newLimit) {
var newParams = this.copy_();
newParams.limitSet_ = true;
newParams.limit_ = newLimit;
newParams.viewFrom_ = QueryParams.WIRE_PROTOCOL_CONSTANTS_.VIEW_FROM_RIGHT;
return newParams;
};
/**
* @param {*} indexValue
* @param {?string=} key
* @return {!QueryParams}
*/
QueryParams.prototype.startAt = function (indexValue, key) {
var newParams = this.copy_();
newParams.startSet_ = true;
if (indexValue === undefined) {
indexValue = null;
}
newParams.indexStartValue_ = indexValue;
if (key != null) {
newParams.startNameSet_ = true;
newParams.indexStartName_ = key;
}
else {
newParams.startNameSet_ = false;
newParams.indexStartName_ = '';
}
return newParams;
};
QueryParams.prototype.startAfter = function (indexValue, key) {
var params;
if (this.index_ === KEY_INDEX) {
if (typeof indexValue === 'string') {
indexValue = successor(indexValue);
}
params = this.startAt(indexValue, key);
}
else {
var childKey = void 0;
if (key == null) {
childKey = MAX_NAME;
}
else {
childKey = successor(key);
}
params = this.startAt(indexValue, childKey);
}
params.startAfterSet_ = true;
return params;
};
/**
* @param {*} indexValue
* @param {?string=} key
* @return {!QueryParams}
*/
QueryParams.prototype.endAt = function (indexValue, key) {
var newParams = this.copy_();
newParams.endSet_ = true;
if (indexValue === undefined) {
indexValue = null;
}
newParams.indexEndValue_ = indexValue;
if (key !== undefined) {
newParams.endNameSet_ = true;
newParams.indexEndName_ = key;
}
else {
newParams.endNameSet_ = false;
newParams.indexEndName_ = '';
}
return newParams;
};
QueryParams.prototype.endBefore = function (indexValue, key) {
var childKey;
var params;
if (this.index_ === KEY_INDEX) {
if (typeof indexValue === 'string') {
indexValue = predecessor(indexValue);
}
params = this.endAt(indexValue, key);
}
else {
if (key == null) {
childKey = MIN_NAME;
}
else {
childKey = predecessor(key);
}
params = this.endAt(indexValue, childKey);
}
params.endBeforeSet_ = true;
return params;
};
/**
* @param {!Index} index
* @return {!QueryParams}
*/
QueryParams.prototype.orderBy = function (index) {
var newParams = this.copy_();
newParams.index_ = index;
return newParams;
};
/**
* @return {!Object}
*/
QueryParams.prototype.getQueryObject = function () {
var WIRE_PROTOCOL_CONSTANTS = QueryParams.WIRE_PROTOCOL_CONSTANTS_;
var obj = {};
if (this.startSet_) {
obj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_VALUE] = this.indexStartValue_;
if (this.startNameSet_) {
obj[WIRE_PROTOCOL_CONSTANTS.INDEX_START_NAME] = this.indexStartName_;
}
}
if (this.endSet_) {
obj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_VALUE] = this.indexEndValue_;
if (this.endNameSet_) {
obj[WIRE_PROTOCOL_CONSTANTS.INDEX_END_NAME] = this.indexEndName_;
}
}
if (this.limitSet_) {
obj[WIRE_PROTOCOL_CONSTANTS.LIMIT] = this.limit_;
var viewFrom = this.viewFrom_;
if (viewFrom === '') {
if (this.isViewFromLeft()) {
viewFrom = WIRE_PROTOCOL_CONSTANTS.VIEW_FROM_LEFT;
}
else {
viewFrom = WIRE_PROTOCOL_CONSTANTS.VIEW_FROM_RIGHT;
}
}
obj[WIRE_PROTOCOL_CONSTANTS.VIEW_FROM] = viewFrom;
}
// For now, priority index is the default, so we only specify if it's some other index
if (this.index_ !== PRIORITY_INDEX) {
obj[WIRE_PROTOCOL_CONSTANTS.INDEX] = this.index_.toString();
}
return obj;
};
/**
* @return {boolean}
*/
QueryParams.prototype.loadsAllData = function () {
return !(this.startSet_ || this.endSet_ || this.limitSet_);
};
/**
* @return {boolean}
*/
QueryParams.prototype.isDefault = function () {
return this.loadsAllData() && this.index_ === PRIORITY_INDEX;
};
/**
* @return {!NodeFilter}
*/
QueryParams.prototype.getNodeFilter = function () {
if (this.loadsAllData()) {
return new IndexedFilter(this.getIndex());
}
else if (this.hasLimit()) {
return new LimitedFilter(this);
}
else {
return new RangedFilter(this);
}
};
/**
* Returns a set of REST query string parameters representing this query.
*
* @return {!Object.} query string parameters
*/
QueryParams.prototype.toRestQueryStringParameters = function () {
var REST_CONSTANTS = QueryParams.REST_QUERY_CONSTANTS_;
var qs = {};
if (this.isDefault()) {
return qs;
}
var orderBy;
if (this.index_ === PRIORITY_INDEX) {
orderBy = REST_CONSTANTS.PRIORITY_INDEX;
}
else if (this.index_ === VALUE_INDEX) {
orderBy = REST_CONSTANTS.VALUE_INDEX;
}
else if (this.index_ === KEY_INDEX) {
orderBy = REST_CONSTANTS.KEY_INDEX;
}
else {
assert(this.index_ instanceof PathIndex, 'Unrecognized index type!');
orderBy = this.index_.toString();
}
qs[REST_CONSTANTS.ORDER_BY] = stringify(orderBy);
if (this.startSet_) {
qs[REST_CONSTANTS.START_AT] = stringify(this.indexStartValue_);
if (this.startNameSet_) {
qs[REST_CONSTANTS.START_AT] += ',' + stringify(this.indexStartName_);
}
}
if (this.endSet_) {
qs[REST_CONSTANTS.END_AT] = stringify(this.indexEndValue_);
if (this.endNameSet_) {
qs[REST_CONSTANTS.END_AT] += ',' + stringify(this.indexEndName_);
}
}
if (this.limitSet_) {
if (this.isViewFromLeft()) {
qs[REST_CONSTANTS.LIMIT_TO_FIRST] = this.limit_;
}
else {
qs[REST_CONSTANTS.LIMIT_TO_LAST] = this.limit_;
}
}
return qs;
};
/**
* Wire Protocol Constants
* @const
* @enum {string}
* @private
*/
QueryParams.WIRE_PROTOCOL_CONSTANTS_ = {
INDEX_START_VALUE: 'sp',
INDEX_START_NAME: 'sn',
INDEX_END_VALUE: 'ep',
INDEX_END_NAME: 'en',
LIMIT: 'l',
VIEW_FROM: 'vf',
VIEW_FROM_LEFT: 'l',
VIEW_FROM_RIGHT: 'r',
INDEX: 'i'
};
/**
* REST Query Constants
* @const
* @enum {string}
* @private
*/
QueryParams.REST_QUERY_CONSTANTS_ = {
ORDER_BY: 'orderBy',
PRIORITY_INDEX: '$priority',
VALUE_INDEX: '$value',
KEY_INDEX: '$key',
START_AT: 'startAt',
END_AT: 'endAt',
LIMIT_TO_FIRST: 'limitToFirst',
LIMIT_TO_LAST: 'limitToLast'
};
/**
* Default, empty query parameters
* @type {!QueryParams}
* @const
*/
QueryParams.DEFAULT = new QueryParams();
return QueryParams;
}());
/**
* @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 Reference = /** @class */ (function (_super) {
__extends$1(Reference, _super);
/**
* Call options:
* new Reference(Repo, Path) or
* new Reference(url: string, string|RepoManager)
*
* Externally - this is the firebase.database.Reference type.
*
* @param {!Repo} repo
* @param {(!Path)} path
* @extends {Query}
*/
function Reference(repo, path) {
var _this = this;
if (!(repo instanceof Repo)) {
throw new Error('new Reference() no longer supported - use app.database().');
}
// call Query's constructor, passing in the repo and path.
_this = _super.call(this, repo, path, QueryParams.DEFAULT, false) || this;
return _this;
}
/** @return {?string} */
Reference.prototype.getKey = function () {
validateArgCount('Reference.key', 0, 0, arguments.length);
if (this.path.isEmpty()) {
return null;
}
else {
return this.path.getBack();
}
};
/**
* @param {!(string|Path)} pathString
* @return {!Reference}
*/
Reference.prototype.child = function (pathString) {
validateArgCount('Reference.child', 1, 1, arguments.length);
if (typeof pathString === 'number') {
pathString = String(pathString);
}
else if (!(pathString instanceof Path)) {
if (this.path.getFront() === null) {
validateRootPathString('Reference.child', 1, pathString, false);
}
else {
validatePathString('Reference.child', 1, pathString, false);
}
}
return new Reference(this.repo, this.path.child(pathString));
};
/** @return {?Reference} */
Reference.prototype.getParent = function () {
validateArgCount('Reference.parent', 0, 0, arguments.length);
var parentPath = this.path.parent();
return parentPath === null ? null : new Reference(this.repo, parentPath);
};
/** @return {!Reference} */
Reference.prototype.getRoot = function () {
validateArgCount('Reference.root', 0, 0, arguments.length);
var ref = this;
while (ref.getParent() !== null) {
ref = ref.getParent();
}
return ref;
};
/** @return {!Database} */
Reference.prototype.databaseProp = function () {
return this.repo.database;
};
/**
* @param {*} newVal
* @param {function(?Error)=} onComplete
* @return {!Promise}
*/
Reference.prototype.set = function (newVal, onComplete) {
validateArgCount('Reference.set', 1, 2, arguments.length);
validateWritablePath('Reference.set', this.path);
validateFirebaseDataArg('Reference.set', 1, newVal, this.path, false);
validateCallback('Reference.set', 2, onComplete, true);
var deferred = new Deferred();
this.repo.setWithPriority(this.path, newVal,
/*priority=*/ null, deferred.wrapCallback(onComplete));
return deferred.promise;
};
/**
* @param {!Object} objectToMerge
* @param {function(?Error)=} onComplete
* @return {!Promise}
*/
Reference.prototype.update = function (objectToMerge, onComplete) {
validateArgCount('Reference.update', 1, 2, arguments.length);
validateWritablePath('Reference.update', this.path);
if (Array.isArray(objectToMerge)) {
var newObjectToMerge = {};
for (var i = 0; i < objectToMerge.length; ++i) {
newObjectToMerge['' + i] = objectToMerge[i];
}
objectToMerge = newObjectToMerge;
warn('Passing an Array to Firebase.update() is deprecated. ' +
'Use set() if you want to overwrite the existing data, or ' +
'an Object with integer keys if you really do want to ' +
'only update some of the children.');
}
validateFirebaseMergeDataArg('Reference.update', 1, objectToMerge, this.path, false);
validateCallback('Reference.update', 2, onComplete, true);
var deferred = new Deferred();
this.repo.update(this.path, objectToMerge, deferred.wrapCallback(onComplete));
return deferred.promise;
};
/**
* @param {*} newVal
* @param {string|number|null} newPriority
* @param {function(?Error)=} onComplete
* @return {!Promise}
*/
Reference.prototype.setWithPriority = function (newVal, newPriority, onComplete) {
validateArgCount('Reference.setWithPriority', 2, 3, arguments.length);
validateWritablePath('Reference.setWithPriority', this.path);
validateFirebaseDataArg('Reference.setWithPriority', 1, newVal, this.path, false);
validatePriority('Reference.setWithPriority', 2, newPriority, false);
validateCallback('Reference.setWithPriority', 3, onComplete, true);
if (this.getKey() === '.length' || this.getKey() === '.keys') {
throw ('Reference.setWithPriority failed: ' +
this.getKey() +
' is a read-only object.');
}
var deferred = new Deferred();
this.repo.setWithPriority(this.path, newVal, newPriority, deferred.wrapCallback(onComplete));
return deferred.promise;
};
/**
* @param {function(?Error)=} onComplete
* @return {!Promise}
*/
Reference.prototype.remove = function (onComplete) {
validateArgCount('Reference.remove', 0, 1, arguments.length);
validateWritablePath('Reference.remove', this.path);
validateCallback('Reference.remove', 1, onComplete, true);
return this.set(null, onComplete);
};
/**
* @param {function(*):*} transactionUpdate
* @param {(function(?Error, boolean, ?DataSnapshot))=} onComplete
* @param {boolean=} applyLocally
* @return {!Promise}
*/
Reference.prototype.transaction = function (transactionUpdate, onComplete, applyLocally) {
validateArgCount('Reference.transaction', 1, 3, arguments.length);
validateWritablePath('Reference.transaction', this.path);
validateCallback('Reference.transaction', 1, transactionUpdate, false);
validateCallback('Reference.transaction', 2, onComplete, true);
// NOTE: applyLocally is an internal-only option for now. We need to decide if we want to keep it and how
// to expose it.
validateBoolean('Reference.transaction', 3, applyLocally, true);
if (this.getKey() === '.length' || this.getKey() === '.keys') {
throw ('Reference.transaction failed: ' +
this.getKey() +
' is a read-only object.');
}
if (applyLocally === undefined) {
applyLocally = true;
}
var deferred = new Deferred();
if (typeof onComplete === 'function') {
deferred.promise.catch(function () { });
}
var promiseComplete = function (error, committed, snapshot) {
if (error) {
deferred.reject(error);
}
else {
deferred.resolve(new TransactionResult(committed, snapshot));
}
if (typeof onComplete === 'function') {
onComplete(error, committed, snapshot);
}
};
this.repo.startTransaction(this.path, transactionUpdate, promiseComplete, applyLocally);
return deferred.promise;
};
/**
* @param {string|number|null} priority
* @param {function(?Error)=} onComplete
* @return {!Promise}
*/
Reference.prototype.setPriority = function (priority, onComplete) {
validateArgCount('Reference.setPriority', 1, 2, arguments.length);
validateWritablePath('Reference.setPriority', this.path);
validatePriority('Reference.setPriority', 1, priority, false);
validateCallback('Reference.setPriority', 2, onComplete, true);
var deferred = new Deferred();
this.repo.setWithPriority(this.path.child('.priority'), priority, null, deferred.wrapCallback(onComplete));
return deferred.promise;
};
/**
* @param {*=} value
* @param {function(?Error)=} onComplete
* @return {!Reference}
*/
Reference.prototype.push = function (value, onComplete) {
validateArgCount('Reference.push', 0, 2, arguments.length);
validateWritablePath('Reference.push', this.path);
validateFirebaseDataArg('Reference.push', 1, value, this.path, true);
validateCallback('Reference.push', 2, onComplete, true);
var now = this.repo.serverTime();
var name = nextPushId(now);
// push() returns a ThennableReference whose promise is fulfilled with a regular Reference.
// We use child() to create handles to two different references. The first is turned into a
// ThennableReference below by adding then() and catch() methods and is used as the
// return value of push(). The second remains a regular Reference and is used as the fulfilled
// value of the first ThennableReference.
var thennablePushRef = this.child(name);
var pushRef = this.child(name);
var promise;
if (value != null) {
promise = thennablePushRef.set(value, onComplete).then(function () { return pushRef; });
}
else {
promise = Promise.resolve(pushRef);
}
thennablePushRef.then = promise.then.bind(promise);
thennablePushRef.catch = promise.then.bind(promise, undefined);
if (typeof onComplete === 'function') {
promise.catch(function () { });
}
return thennablePushRef;
};
/**
* @return {!OnDisconnect}
*/
Reference.prototype.onDisconnect = function () {
validateWritablePath('Reference.onDisconnect', this.path);
return new OnDisconnect(this.repo, this.path);
};
Object.defineProperty(Reference.prototype, "database", {
get: function () {
return this.databaseProp();
},
enumerable: false,
configurable: true
});
Object.defineProperty(Reference.prototype, "key", {
get: function () {
return this.getKey();
},
enumerable: false,
configurable: true
});
Object.defineProperty(Reference.prototype, "parent", {
get: function () {
return this.getParent();
},
enumerable: false,
configurable: true
});
Object.defineProperty(Reference.prototype, "root", {
get: function () {
return this.getRoot();
},
enumerable: false,
configurable: true
});
return Reference;
}(Query));
/**
* Define reference constructor in various modules
*
* We are doing this here to avoid several circular
* dependency issues
*/
Query.__referenceConstructor = Reference;
SyncPoint.__referenceConstructor = Reference;
/**
* @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.
*/
/**
* Class representing a firebase database.
* @implements {FirebaseService}
*/
var Database = /** @class */ (function () {
/**
* The constructor should not be called by users of our public API.
* @param {!Repo} repoInternal_
*/
function Database(repoInternal_) {
var _this = this;
this.repoInternal_ = repoInternal_;
/** Track if the instance has been used (root or repo accessed) */
this.instanceStarted_ = false;
this.INTERNAL = {
delete: function () { return __awaiter(_this, void 0, void 0, function () {
return __generator(this, function (_a) {
this.checkDeleted_('delete');
RepoManager.getInstance().deleteRepo(this.repo_);
this.repoInternal_ = null;
this.rootInternal_ = null;
return [2 /*return*/];
});
}); }
};
if (!(repoInternal_ instanceof Repo)) {
fatal("Don't call new Database() directly - please use firebase.database().");
}
}
Object.defineProperty(Database.prototype, "repo_", {
get: function () {
if (!this.instanceStarted_) {
this.repoInternal_.start();
this.instanceStarted_ = true;
}
return this.repoInternal_;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Database.prototype, "root_", {
get: function () {
if (!this.rootInternal_) {
this.rootInternal_ = new Reference(this.repo_, Path.Empty);
}
return this.rootInternal_;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Database.prototype, "app", {
get: function () {
return this.repo_.app;
},
enumerable: false,
configurable: true
});
/**
* 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)
*/
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.
* @param {string} url
* @return {!Reference} 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());
};
/**
* @param {string} apiName
*/
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');
this.repo_.interrupt();
};
Database.prototype.goOnline = function () {
validateArgCount('database.goOnline', 0, 0, arguments.length);
this.checkDeleted_('goOnline');
this.repo_.resume();
};
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 () {
/**
* @template T
* @param {string=} name_ Optional name of the node.
* @param {Tree=} parent_ Optional parent node.
* @param {TreeNode=} 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 {!(string|Path)} pathObj Path to look up.
* @return {!Tree.} 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 = path.getFront();
while (next !== null) {
var childNode = safeGet(child.node_.children, next) || new TreeNode();
child = new Tree(next, child, childNode);
path = path.popFront();
next = path.getFront();
}
return child;
};
/**
* Returns the data associated with this tree node.
*
* @return {?T} The data or null if no data exists.
*/
Tree.prototype.getValue = function () {
return this.node_.value;
};
/**
* Sets data to this tree node.
*
* @param {!T} 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 {boolean} Whether the tree has any children.
*/
Tree.prototype.hasChildren = function () {
return this.node_.childCount > 0;
};
/**
* @return {boolean} Whether the 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 {function(!Tree.)} 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 {function(!Tree.)} action Action to be called for each child.
* @param {boolean=} includeSelf Whether to call action on this node as well. Defaults to
* false.
* @param {boolean=} 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 {function(!Tree.)} action Action to be called on each parent; return
* true to abort.
* @param {boolean=} includeSelf Whether to call action on this node as well.
* @return {boolean} 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 {function(!Tree.)} 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 {!Path} 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 {string} The name of the tree node.
*/
Tree.prototype.name = function () {
return this.name_;
};
/**
* @return {?Tree} 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.
*
* @private
*/
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 {string} childName The name of the child to update.
* @param {!Tree.} child The child to update.
* @private
*/
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;
var TransactionStatus;
(function (TransactionStatus) {
// We've run the transaction and updated transactionResultData_ with the result, but it isn't currently sent to the
// server. A transaction will go from RUN -> SENT -> RUN if it comes back from the server as rejected due to
// mismatched hash.
TransactionStatus[TransactionStatus["RUN"] = 0] = "RUN";
// We've run the transaction and sent it to the server and it's currently outstanding (hasn't come back as accepted
// or rejected yet).
TransactionStatus[TransactionStatus["SENT"] = 1] = "SENT";
// Temporary state used to mark completed transactions (whether successful or aborted). The transaction will be
// removed when we get a chance to prune completed ones.
TransactionStatus[TransactionStatus["COMPLETED"] = 2] = "COMPLETED";
// Used when an already-sent transaction needs to be aborted (e.g. due to a conflicting set() call that was made).
// If it comes back as unsuccessful, we'll abort it.
TransactionStatus[TransactionStatus["SENT_NEEDS_ABORT"] = 3] = "SENT_NEEDS_ABORT";
// Temporary state used to mark transactions that need to be aborted.
TransactionStatus[TransactionStatus["NEEDS_ABORT"] = 4] = "NEEDS_ABORT";
})(TransactionStatus || (TransactionStatus = {}));
/**
* 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();
}
Repo.prototype.start = function () {
var _this = this;
this.stats_ = StatsManager.getCollection(this.repoInfo_);
if (this.forceRestClient_ || beingCrawled()) {
this.server_ = new ReadonlyRestClient(this.repoInfo_, this.onDataUpdate_.bind(this), this.authTokenProvider_);
// Minor hack: Fire onConnect immediately, since there's no actual connection.
setTimeout(this.onConnectStatus_.bind(this, true), 0);
}
else {
var authOverride = this.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);
}
}
this.persistentConnection_ = new PersistentConnection(this.repoInfo_, this.app.options.appId, this.onDataUpdate_.bind(this), this.onConnectStatus_.bind(this), this.onServerInfoUpdate_.bind(this), this.authTokenProvider_, authOverride);
this.server_ = this.persistentConnection_;
}
this.authTokenProvider_.addTokenChangeListener(function (token) {
_this.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.
this.statsReporter_ = StatsManager.getOrCreateReporter(this.repoInfo_, function () { return new StatsReporter(_this.stats_, _this.server_); });
// Used for .info.
this.infoData_ = new SnapshotHolder();
this.infoSyncTree_ = new SyncTree({
startListening: function (query, tag, currentHashFn, onComplete) {
var infoEvents = [];
var node = _this.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 = _this.infoSyncTree_.applyServerOverwrite(query.path, node);
setTimeout(function () {
onComplete('ok');
}, 0);
}
return infoEvents;
},
stopListening: function () { }
});
this.updateInfo_('connected', false);
this.serverSyncTree_ = new SyncTree({
startListening: function (query, tag, currentHashFn, onComplete) {
_this.server_.listen(query, currentHashFn, tag, function (status, data) {
var events = onComplete(status, data);
_this.eventQueue_.raiseEventsForChangedPath(query.path, events);
});
// No synchronous events for network-backed sync trees
return [];
},
stopListening: function (query, tag) {
_this.server_.unlisten(query, tag);
}
});
};
/**
* @return The URL corresponding to the root of this Firebase.
*/
Repo.prototype.toString = function () {
return ((this.repoInfo_.secure ? 'https://' : 'http://') + this.repoInfo_.host);
};
/**
* @return The namespace represented by the repo.
*/
Repo.prototype.name = function () {
return this.repoInfo_.namespace;
};
/**
* @return The time in milliseconds, taking the server offset into account if we have one.
*/
Repo.prototype.serverTime = function () {
var offsetNode = this.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.
*/
Repo.prototype.generateServerValues = function () {
return generateWithValues({
timestamp: this.serverTime()
});
};
/**
* Called by realtime when we get new messages from the server.
*/
Repo.prototype.onDataUpdate_ = function (pathString, data, isMerge, tag) {
// For testing.
this.dataUpdateCount++;
var path = new Path(pathString);
data = this.interceptServerDataCallback_
? this.interceptServerDataCallback_(pathString, data)
: data;
var events = [];
if (tag) {
if (isMerge) {
var taggedChildren = map(data, function (raw) { return nodeFromJSON$1(raw); });
events = this.serverSyncTree_.applyTaggedQueryMerge(path, taggedChildren, tag);
}
else {
var taggedSnap = nodeFromJSON$1(data);
events = this.serverSyncTree_.applyTaggedQueryOverwrite(path, taggedSnap, tag);
}
}
else if (isMerge) {
var changedChildren = map(data, function (raw) { return nodeFromJSON$1(raw); });
events = this.serverSyncTree_.applyServerMerge(path, changedChildren);
}
else {
var snap = nodeFromJSON$1(data);
events = this.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 = this.rerunTransactions_(path);
}
this.eventQueue_.raiseEventsForChangedPath(affectedPath, events);
};
// TODO: This should be @private but it's used by test_access.js and internal.js
Repo.prototype.interceptServerData_ = function (callback) {
this.interceptServerDataCallback_ = callback;
};
Repo.prototype.onConnectStatus_ = function (connectStatus) {
this.updateInfo_('connected', connectStatus);
if (connectStatus === false) {
this.runOnDisconnectEvents_();
}
};
Repo.prototype.onServerInfoUpdate_ = function (updates) {
var _this = this;
each(updates, function (key, value) {
_this.updateInfo_(key, value);
});
};
Repo.prototype.updateInfo_ = function (pathString, value) {
var path = new Path('/.info/' + pathString);
var newNode = nodeFromJSON$1(value);
this.infoData_.updateSnapshot(path, newNode);
var events = this.infoSyncTree_.applyServerOverwrite(path, newNode);
this.eventQueue_.raiseEventsForChangedPath(path, events);
};
Repo.prototype.getNextWriteId_ = function () {
return this.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.
*/
Repo.prototype.getValue = function (query) {
var _this = this;
// Only active queries are cached. There is no persisted cache.
var cached = this.serverSyncTree_.calcCompleteEventCache(query.path);
if (!cached.isEmpty()) {
return Promise.resolve(new DataSnapshot(cached, query.getRef(), query.getQueryParams().getIndex()));
}
return this.server_.get(query).then(function (payload) {
var node = nodeFromJSON$1(payload);
var events = _this.serverSyncTree_.applyServerOverwrite(query.path, node);
_this.eventQueue_.raiseEventsAtPath(query.path, events);
return Promise.resolve(new DataSnapshot(node, query.getRef(), query.getQueryParams().getIndex()));
}, function (err) {
_this.log_('get for query ' + stringify(query) + ' failed: ' + err);
return Promise.reject(new Error(err));
});
};
Repo.prototype.setWithPriority = function (path, newVal, newPriority, onComplete) {
var _this = this;
this.log_('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 = this.generateServerValues();
var newNodeUnresolved = nodeFromJSON$1(newVal, newPriority);
var existing = this.serverSyncTree_.calcCompleteEventCache(path);
var newNode = resolveDeferredValueSnapshot(newNodeUnresolved, existing, serverValues);
var writeId = this.getNextWriteId_();
var events = this.serverSyncTree_.applyUserOverwrite(path, newNode, writeId, true);
this.eventQueue_.queueEvents(events);
this.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 = _this.serverSyncTree_.ackUserWrite(writeId, !success);
_this.eventQueue_.raiseEventsForChangedPath(path, clearEvents);
_this.callOnCompleteCallback(onComplete, status, errorReason);
});
var affectedPath = this.abortTransactions_(path);
this.rerunTransactions_(affectedPath);
// We queued the events above, so just flush the queue here
this.eventQueue_.raiseEventsForChangedPath(affectedPath, []);
};
Repo.prototype.update = function (path, childrenToMerge, onComplete) {
var _this = this;
this.log_('update', { path: path.toString(), value: childrenToMerge });
// Start with our existing data and merge each child into it.
var empty = true;
var serverValues = this.generateServerValues();
var changedChildren = {};
each(childrenToMerge, function (changedKey, changedValue) {
empty = false;
changedChildren[changedKey] = resolveDeferredValueTree(path.child(changedKey), nodeFromJSON$1(changedValue), _this.serverSyncTree_, serverValues);
});
if (!empty) {
var writeId_1 = this.getNextWriteId_();
var events = this.serverSyncTree_.applyUserMerge(path, changedChildren, writeId_1);
this.eventQueue_.queueEvents(events);
this.server_.merge(path.toString(), childrenToMerge, function (status, errorReason) {
var success = status === 'ok';
if (!success) {
warn('update at ' + path + ' failed: ' + status);
}
var clearEvents = _this.serverSyncTree_.ackUserWrite(writeId_1, !success);
var affectedPath = clearEvents.length > 0 ? _this.rerunTransactions_(path) : path;
_this.eventQueue_.raiseEventsForChangedPath(affectedPath, clearEvents);
_this.callOnCompleteCallback(onComplete, status, errorReason);
});
each(childrenToMerge, function (changedPath) {
var affectedPath = _this.abortTransactions_(path.child(changedPath));
_this.rerunTransactions_(affectedPath);
});
// We queued the events above, so just flush the queue here
this.eventQueue_.raiseEventsForChangedPath(path, []);
}
else {
log("update() called with empty data. Don't do anything.");
this.callOnCompleteCallback(onComplete, 'ok');
}
};
/**
* Applies all of the changes stored up in the onDisconnect_ tree.
*/
Repo.prototype.runOnDisconnectEvents_ = function () {
var _this = this;
this.log_('onDisconnectEvents');
var serverValues = this.generateServerValues();
var resolvedOnDisconnectTree = new SparseSnapshotTree();
this.onDisconnect_.forEachTree(Path.Empty, function (path, node) {
var resolved = resolveDeferredValueTree(path, node, _this.serverSyncTree_, serverValues);
resolvedOnDisconnectTree.remember(path, resolved);
});
var events = [];
resolvedOnDisconnectTree.forEachTree(Path.Empty, function (path, snap) {
events = events.concat(_this.serverSyncTree_.applyServerOverwrite(path, snap));
var affectedPath = _this.abortTransactions_(path);
_this.rerunTransactions_(affectedPath);
});
this.onDisconnect_ = new SparseSnapshotTree();
this.eventQueue_.raiseEventsForChangedPath(Path.Empty, events);
};
Repo.prototype.onDisconnectCancel = function (path, onComplete) {
var _this = this;
this.server_.onDisconnectCancel(path.toString(), function (status, errorReason) {
if (status === 'ok') {
_this.onDisconnect_.forget(path);
}
_this.callOnCompleteCallback(onComplete, status, errorReason);
});
};
Repo.prototype.onDisconnectSet = function (path, value, onComplete) {
var _this = this;
var newNode = nodeFromJSON$1(value);
this.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), function (status, errorReason) {
if (status === 'ok') {
_this.onDisconnect_.remember(path, newNode);
}
_this.callOnCompleteCallback(onComplete, status, errorReason);
});
};
Repo.prototype.onDisconnectSetWithPriority = function (path, value, priority, onComplete) {
var _this = this;
var newNode = nodeFromJSON$1(value, priority);
this.server_.onDisconnectPut(path.toString(), newNode.val(/*export=*/ true), function (status, errorReason) {
if (status === 'ok') {
_this.onDisconnect_.remember(path, newNode);
}
_this.callOnCompleteCallback(onComplete, status, errorReason);
});
};
Repo.prototype.onDisconnectUpdate = function (path, childrenToMerge, onComplete) {
var _this = this;
if (isEmpty(childrenToMerge)) {
log("onDisconnect().update() called with empty data. Don't do anything.");
this.callOnCompleteCallback(onComplete, 'ok');
return;
}
this.server_.onDisconnectMerge(path.toString(), childrenToMerge, function (status, errorReason) {
if (status === 'ok') {
each(childrenToMerge, function (childName, childNode) {
var newChildNode = nodeFromJSON$1(childNode);
_this.onDisconnect_.remember(path.child(childName), newChildNode);
});
}
_this.callOnCompleteCallback(onComplete, status, errorReason);
});
};
Repo.prototype.addEventCallbackForQuery = function (query, eventRegistration) {
var events;
if (query.path.getFront() === '.info') {
events = this.infoSyncTree_.addEventRegistration(query, eventRegistration);
}
else {
events = this.serverSyncTree_.addEventRegistration(query, eventRegistration);
}
this.eventQueue_.raiseEventsAtPath(query.path, events);
};
Repo.prototype.removeEventCallbackForQuery = function (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 (query.path.getFront() === '.info') {
events = this.infoSyncTree_.removeEventRegistration(query, eventRegistration);
}
else {
events = this.serverSyncTree_.removeEventRegistration(query, eventRegistration);
}
this.eventQueue_.raiseEventsAtPath(query.path, events);
};
Repo.prototype.interrupt = function () {
if (this.persistentConnection_) {
this.persistentConnection_.interrupt(INTERRUPT_REASON);
}
};
Repo.prototype.resume = function () {
if (this.persistentConnection_) {
this.persistentConnection_.resume(INTERRUPT_REASON);
}
};
Repo.prototype.stats = function (showDelta) {
if (showDelta === void 0) { showDelta = false; }
if (typeof console === 'undefined') {
return;
}
var stats;
if (showDelta) {
if (!this.statsListener_) {
this.statsListener_ = new StatsListener(this.stats_);
}
stats = this.statsListener_.get();
}
else {
stats = this.stats_.get();
}
var longestName = Object.keys(stats).reduce(function (previousValue, currentValue) {
return Math.max(currentValue.length, previousValue);
}, 0);
each(stats, function (stat, value) {
var paddedStat = stat;
// pad stat names to be the same length (plus 2 extra spaces).
for (var i = stat.length; i < longestName + 2; i++) {
paddedStat += ' ';
}
console.log(paddedStat + value);
});
};
Repo.prototype.statsIncrementCounter = function (metric) {
this.stats_.incrementCounter(metric);
this.statsReporter_.includeStat(metric);
};
Repo.prototype.log_ = function () {
var varArgs = [];
for (var _i = 0; _i < arguments.length; _i++) {
varArgs[_i] = arguments[_i];
}
var prefix = '';
if (this.persistentConnection_) {
prefix = this.persistentConnection_.id + ':';
}
log.apply(void 0, __spread([prefix], varArgs));
};
Repo.prototype.callOnCompleteCallback = function (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);
}
});
}
};
Object.defineProperty(Repo.prototype, "database", {
get: function () {
return this.__database || (this.__database = new Database(this));
},
enumerable: false,
configurable: true
});
/**
* 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
*/
Repo.prototype.startTransaction = function (path, transactionUpdate, onComplete, applyLocally) {
this.log_('transaction on ' + path);
// Add a watch to make sure we get server updates.
var valueCallback = function () { };
var watchRef = new Reference(this, 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 = this.getLatestState_(path);
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(this, 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 = TransactionStatus.RUN;
var queueNode = this.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 = this.serverSyncTree_.calcCompleteEventCache(path) ||
ChildrenNode.EMPTY_NODE;
priorityForNode = currentNode.getPriority().val();
}
var serverValues = this.generateServerValues();
var newNodeUnresolved = nodeFromJSON$1(newVal, priorityForNode);
var newNode = resolveDeferredValueSnapshot(newNodeUnresolved, currentState, serverValues);
transaction.currentOutputSnapshotRaw = newNodeUnresolved;
transaction.currentOutputSnapshotResolved = newNode;
transaction.currentWriteId = this.getNextWriteId_();
var events = this.serverSyncTree_.applyUserOverwrite(path, newNode, transaction.currentWriteId, transaction.applyLocally);
this.eventQueue_.raiseEventsForChangedPath(path, events);
this.sendReadyTransactions_();
}
};
/**
* @param excludeSets A specific set to exclude
*/
Repo.prototype.getLatestState_ = function (path, excludeSets) {
return (this.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.
*/
Repo.prototype.sendReadyTransactions_ = function (node) {
var _this = this;
if (node === void 0) { node = this.transactionQueueTree_; }
// Before recursing, make sure any completed transactions are removed.
if (!node) {
this.pruneCompletedTransactionsBelowNode_(node);
}
if (node.getValue() !== null) {
var queue = this.buildTransactionQueue_(node);
assert(queue.length > 0, 'Sending zero length transaction queue');
var allRun = queue.every(function (transaction) {
return transaction.status === TransactionStatus.RUN;
});
// If they're all run (and not sent), we can send them. Else, we must wait.
if (allRun) {
this.sendTransactionQueue_(node.path(), queue);
}
}
else if (node.hasChildren()) {
node.forEachChild(function (childNode) {
_this.sendReadyTransactions_(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.
*/
Repo.prototype.sendTransactionQueue_ = function (path, queue) {
var _this = this;
// Mark transactions as sent and increment retry count!
var setsToIgnore = queue.map(function (txn) {
return txn.currentWriteId;
});
var latestState = this.getLatestState_(path, setsToIgnore);
var snapToSend = latestState;
var latestHash = latestState.hash();
for (var i = 0; i < queue.length; i++) {
var txn = queue[i];
assert(txn.status === TransactionStatus.RUN, 'tryToSendTransactionQueue_: items in queue should all be run.');
txn.status = TransactionStatus.SENT;
txn.retryCount++;
var relativePath = Path.relativePath(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.
this.server_.put(pathToSend.toString(), dataToSend, function (status) {
_this.log_('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 = TransactionStatus.COMPLETED;
events = events.concat(_this.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(_this, 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.
_this.pruneCompletedTransactionsBelowNode_(_this.transactionQueueTree_.subTree(path));
// There may be pending transactions that we can now send.
_this.sendReadyTransactions_();
_this.eventQueue_.raiseEventsForChangedPath(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 === TransactionStatus.SENT_NEEDS_ABORT) {
queue[i].status = TransactionStatus.NEEDS_ABORT;
}
else {
queue[i].status = TransactionStatus.RUN;
}
}
}
else {
warn('transaction at ' + pathToSend.toString() + ' failed: ' + status);
for (var i = 0; i < queue.length; i++) {
queue[i].status = TransactionStatus.NEEDS_ABORT;
queue[i].abortReason = status;
}
}
_this.rerunTransactions_(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.
*/
Repo.prototype.rerunTransactions_ = function (changedPath) {
var rootMostTransactionNode = this.getAncestorTransactionNode_(changedPath);
var path = rootMostTransactionNode.path();
var queue = this.buildTransactionQueue_(rootMostTransactionNode);
this.rerunTransactionQueue_(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.
*/
Repo.prototype.rerunTransactionQueue_ = function (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 === TransactionStatus.RUN;
});
var setsToIgnore = txnsToRerun.map(function (q) {
return q.currentWriteId;
});
for (var i = 0; i < queue.length; i++) {
var transaction = queue[i];
var relativePath = Path.relativePath(path, transaction.path);
var abortTransaction = false, abortReason = void 0;
assert(relativePath !== null, 'rerunTransactionsUnderNode_: relativePath should not be null.');
if (transaction.status === TransactionStatus.NEEDS_ABORT) {
abortTransaction = true;
abortReason = transaction.abortReason;
events = events.concat(this.serverSyncTree_.ackUserWrite(transaction.currentWriteId, true));
}
else if (transaction.status === TransactionStatus.RUN) {
if (transaction.retryCount >= MAX_TRANSACTION_RETRIES) {
abortTransaction = true;
abortReason = 'maxretry';
events = events.concat(this.serverSyncTree_.ackUserWrite(transaction.currentWriteId, true));
}
else {
// This code reruns a transaction
var currentNode = this.getLatestState_(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 = this.generateServerValues();
var newNodeResolved = resolveDeferredValueSnapshot(newDataNode, currentNode, serverValues);
transaction.currentOutputSnapshotRaw = newDataNode;
transaction.currentOutputSnapshotResolved = newNodeResolved;
transaction.currentWriteId = this.getNextWriteId_();
// Mutates setsToIgnore in place
setsToIgnore.splice(setsToIgnore.indexOf(oldWriteId), 1);
events = events.concat(this.serverSyncTree_.applyUserOverwrite(transaction.path, newNodeResolved, transaction.currentWriteId, transaction.applyLocally));
events = events.concat(this.serverSyncTree_.ackUserWrite(oldWriteId, true));
}
else {
abortTransaction = true;
abortReason = 'nodata';
events = events.concat(this.serverSyncTree_.ackUserWrite(transaction.currentWriteId, true));
}
}
}
this.eventQueue_.raiseEventsForChangedPath(path, events);
events = [];
if (abortTransaction) {
// Abort.
queue[i].status = TransactionStatus.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(this, 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.
this.pruneCompletedTransactionsBelowNode_(this.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.
this.sendReadyTransactions_();
};
/**
* 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.
*/
Repo.prototype.getAncestorTransactionNode_ = function (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 = this.transactionQueueTree_;
front = path.getFront();
while (front !== null && transactionNode.getValue() === null) {
transactionNode = transactionNode.subTree(front);
path = path.popFront();
front = path.getFront();
}
return transactionNode;
};
/**
* Builds the queue of all transactions at or below the specified
* transactionNode.
*
* @param transactionNode
* @return The generated queue.
*/
Repo.prototype.buildTransactionQueue_ = function (transactionNode) {
// Walk any child transaction queues and aggregate them into a single queue.
var transactionQueue = [];
this.aggregateTransactionQueuesForNode_(transactionNode, transactionQueue);
// Sort them by the order the transactions were created.
transactionQueue.sort(function (a, b) {
return a.order - b.order;
});
return transactionQueue;
};
Repo.prototype.aggregateTransactionQueuesForNode_ = function (node, queue) {
var _this = this;
var nodeQueue = node.getValue();
if (nodeQueue !== null) {
for (var i = 0; i < nodeQueue.length; i++) {
queue.push(nodeQueue[i]);
}
}
node.forEachChild(function (child) {
_this.aggregateTransactionQueuesForNode_(child, queue);
});
};
/**
* Remove COMPLETED transactions at or below this node in the transactionQueueTree_.
*/
Repo.prototype.pruneCompletedTransactionsBelowNode_ = function (node) {
var _this = this;
var queue = node.getValue();
if (queue) {
var to = 0;
for (var from = 0; from < queue.length; from++) {
if (queue[from].status !== TransactionStatus.COMPLETED) {
queue[to] = queue[from];
to++;
}
}
queue.length = to;
node.setValue(queue.length > 0 ? queue : null);
}
node.forEachChild(function (childNode) {
_this.pruneCompletedTransactionsBelowNode_(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.
*/
Repo.prototype.abortTransactions_ = function (path) {
var _this = this;
var affectedPath = this.getAncestorTransactionNode_(path).path();
var transactionNode = this.transactionQueueTree_.subTree(path);
transactionNode.forEachAncestor(function (node) {
_this.abortTransactionsOnNode_(node);
});
this.abortTransactionsOnNode_(transactionNode);
transactionNode.forEachDescendant(function (node) {
_this.abortTransactionsOnNode_(node);
});
return affectedPath;
};
/**
* Abort transactions stored in this transaction queue node.
*
* @param node Node to abort transactions for.
*/
Repo.prototype.abortTransactionsOnNode_ = function (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 === TransactionStatus.SENT_NEEDS_ABORT) ;
else if (queue[i].status === TransactionStatus.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 = TransactionStatus.SENT_NEEDS_ABORT;
queue[i].abortReason = 'set';
}
else {
assert(queue[i].status === TransactionStatus.RUN, 'Unexpected transaction status in abort');
// We can abort it immediately.
queue[i].unwatcher();
events = events.concat(this.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.
this.eventQueue_.raiseEventsForChangedPath(node.path(), events);
for (var i = 0; i < callbacks.length; i++) {
exceptionGuard(callbacks[i]);
}
}
};
return Repo;
}());
/**
* @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); });
}
}
/**
* @param {boolean} forceRefresh
* @return {!Promise}
*/
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() {
/**
* @private {!Object.>}
*/
this.repos_ = {};
/**
* If true, new Repos will be created to use ReadonlyRestClient (for testing purposes).
* @private {boolean}
*/
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;
this.repos_[appName][dbUrl].interrupt();
}
}
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;
this.repos_[appName][dbUrl].resume();
}
}
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.
*
* @param {!FirebaseApp} app
* @return {!Database}
*/
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 (!parsedUrl.path.isEmpty()) {
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 repo.database;
};
/**
* Remove the repo and make sure it is disconnected.
*
* @param {!Repo} repo
*/
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.");
}
repo.interrupt();
delete appRepos[repo.key];
};
/**
* Ensures a repo doesn't already exist and then creates one using the
* provided app.
*
* @param {!RepoInfo} repoInfo The metadata about the Repo
* @param {!FirebaseApp} app
* @return {!Repo} 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.
* @param {boolean} forceRestClient
*/
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.DataSnapshot = DataSnapshot;
exports.FirebaseDatabase = FirebaseDatabase;
exports.OnDisconnect = OnDisconnect;
exports.Query = Query;
exports.Reference = Reference;
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