955 lines
34 KiB
JavaScript
955 lines
34 KiB
JavaScript
import { Component, ComponentContainer } from '@firebase/component';
|
|
import { Logger, setUserLogHandler, setLogLevel as setLogLevel$1 } from '@firebase/logger';
|
|
import { ErrorFactory, getDefaultAppConfig, deepEqual, FirebaseError, base64urlEncodeWithoutPadding, isIndexedDBAvailable, validateIndexedDBOpenable } from '@firebase/util';
|
|
export { FirebaseError } from '@firebase/util';
|
|
import { openDB } from 'idb';
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
class PlatformLoggerServiceImpl {
|
|
constructor(container) {
|
|
this.container = container;
|
|
}
|
|
// In initial implementation, this will be called by installations on
|
|
// auth token refresh, and installations will send this string.
|
|
getPlatformInfoString() {
|
|
const providers = this.container.getProviders();
|
|
// Loop through providers and get library/version pairs from any that are
|
|
// version components.
|
|
return providers
|
|
.map(provider => {
|
|
if (isVersionServiceProvider(provider)) {
|
|
const service = provider.getImmediate();
|
|
return `${service.library}/${service.version}`;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
})
|
|
.filter(logString => logString)
|
|
.join(' ');
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param provider check if this provider provides a VersionService
|
|
*
|
|
* NOTE: Using Provider<'app-version'> is a hack to indicate that the provider
|
|
* provides VersionService. The provider is not necessarily a 'app-version'
|
|
* provider.
|
|
*/
|
|
function isVersionServiceProvider(provider) {
|
|
const component = provider.getComponent();
|
|
return (component === null || component === void 0 ? void 0 : component.type) === "VERSION" /* ComponentType.VERSION */;
|
|
}
|
|
|
|
const name$o = "@firebase/app";
|
|
const version$1 = "0.9.27";
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
const logger = new Logger('@firebase/app');
|
|
|
|
const name$n = "@firebase/app-compat";
|
|
|
|
const name$m = "@firebase/analytics-compat";
|
|
|
|
const name$l = "@firebase/analytics";
|
|
|
|
const name$k = "@firebase/app-check-compat";
|
|
|
|
const name$j = "@firebase/app-check";
|
|
|
|
const name$i = "@firebase/auth";
|
|
|
|
const name$h = "@firebase/auth-compat";
|
|
|
|
const name$g = "@firebase/database";
|
|
|
|
const name$f = "@firebase/database-compat";
|
|
|
|
const name$e = "@firebase/functions";
|
|
|
|
const name$d = "@firebase/functions-compat";
|
|
|
|
const name$c = "@firebase/installations";
|
|
|
|
const name$b = "@firebase/installations-compat";
|
|
|
|
const name$a = "@firebase/messaging";
|
|
|
|
const name$9 = "@firebase/messaging-compat";
|
|
|
|
const name$8 = "@firebase/performance";
|
|
|
|
const name$7 = "@firebase/performance-compat";
|
|
|
|
const name$6 = "@firebase/remote-config";
|
|
|
|
const name$5 = "@firebase/remote-config-compat";
|
|
|
|
const name$4 = "@firebase/storage";
|
|
|
|
const name$3 = "@firebase/storage-compat";
|
|
|
|
const name$2 = "@firebase/firestore";
|
|
|
|
const name$1 = "@firebase/firestore-compat";
|
|
|
|
const name = "firebase";
|
|
const version = "10.8.0";
|
|
|
|
/**
|
|
* @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 default app name
|
|
*
|
|
* @internal
|
|
*/
|
|
const DEFAULT_ENTRY_NAME = '[DEFAULT]';
|
|
const PLATFORM_LOG_STRING = {
|
|
[name$o]: 'fire-core',
|
|
[name$n]: 'fire-core-compat',
|
|
[name$l]: 'fire-analytics',
|
|
[name$m]: 'fire-analytics-compat',
|
|
[name$j]: 'fire-app-check',
|
|
[name$k]: 'fire-app-check-compat',
|
|
[name$i]: 'fire-auth',
|
|
[name$h]: 'fire-auth-compat',
|
|
[name$g]: 'fire-rtdb',
|
|
[name$f]: 'fire-rtdb-compat',
|
|
[name$e]: 'fire-fn',
|
|
[name$d]: 'fire-fn-compat',
|
|
[name$c]: 'fire-iid',
|
|
[name$b]: 'fire-iid-compat',
|
|
[name$a]: 'fire-fcm',
|
|
[name$9]: 'fire-fcm-compat',
|
|
[name$8]: 'fire-perf',
|
|
[name$7]: 'fire-perf-compat',
|
|
[name$6]: 'fire-rc',
|
|
[name$5]: 'fire-rc-compat',
|
|
[name$4]: 'fire-gcs',
|
|
[name$3]: 'fire-gcs-compat',
|
|
[name$2]: 'fire-fst',
|
|
[name$1]: 'fire-fst-compat',
|
|
'fire-js': 'fire-js',
|
|
[name]: 'fire-js-all'
|
|
};
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
/**
|
|
* @internal
|
|
*/
|
|
const _apps = new Map();
|
|
/**
|
|
* Registered components.
|
|
*
|
|
* @internal
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const _components = new Map();
|
|
/**
|
|
* @param component - the component being added to this app's container
|
|
*
|
|
* @internal
|
|
*/
|
|
function _addComponent(app, component) {
|
|
try {
|
|
app.container.addComponent(component);
|
|
}
|
|
catch (e) {
|
|
logger.debug(`Component ${component.name} failed to register with FirebaseApp ${app.name}`, e);
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @internal
|
|
*/
|
|
function _addOrOverwriteComponent(app, component) {
|
|
app.container.addOrOverwriteComponent(component);
|
|
}
|
|
/**
|
|
*
|
|
* @param component - the component to register
|
|
* @returns whether or not the component is registered successfully
|
|
*
|
|
* @internal
|
|
*/
|
|
function _registerComponent(component) {
|
|
const componentName = component.name;
|
|
if (_components.has(componentName)) {
|
|
logger.debug(`There were multiple attempts to register component ${componentName}.`);
|
|
return false;
|
|
}
|
|
_components.set(componentName, component);
|
|
// add the component to existing app instances
|
|
for (const app of _apps.values()) {
|
|
_addComponent(app, component);
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
*
|
|
* @param app - FirebaseApp instance
|
|
* @param name - service name
|
|
*
|
|
* @returns the provider for the service with the matching name
|
|
*
|
|
* @internal
|
|
*/
|
|
function _getProvider(app, name) {
|
|
const heartbeatController = app.container
|
|
.getProvider('heartbeat')
|
|
.getImmediate({ optional: true });
|
|
if (heartbeatController) {
|
|
void heartbeatController.triggerHeartbeat();
|
|
}
|
|
return app.container.getProvider(name);
|
|
}
|
|
/**
|
|
*
|
|
* @param app - FirebaseApp instance
|
|
* @param name - service name
|
|
* @param instanceIdentifier - service instance identifier in case the service supports multiple instances
|
|
*
|
|
* @internal
|
|
*/
|
|
function _removeServiceInstance(app, name, instanceIdentifier = DEFAULT_ENTRY_NAME) {
|
|
_getProvider(app, name).clearInstance(instanceIdentifier);
|
|
}
|
|
/**
|
|
* Test only
|
|
*
|
|
* @internal
|
|
*/
|
|
function _clearComponents() {
|
|
_components.clear();
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
const ERRORS = {
|
|
["no-app" /* AppError.NO_APP */]: "No Firebase App '{$appName}' has been created - " +
|
|
'call initializeApp() first',
|
|
["bad-app-name" /* AppError.BAD_APP_NAME */]: "Illegal App name: '{$appName}",
|
|
["duplicate-app" /* AppError.DUPLICATE_APP */]: "Firebase App named '{$appName}' already exists with different options or config",
|
|
["app-deleted" /* AppError.APP_DELETED */]: "Firebase App named '{$appName}' already deleted",
|
|
["no-options" /* AppError.NO_OPTIONS */]: 'Need to provide options, when not being deployed to hosting via source.',
|
|
["invalid-app-argument" /* AppError.INVALID_APP_ARGUMENT */]: 'firebase.{$appName}() takes either no argument or a ' +
|
|
'Firebase App instance.',
|
|
["invalid-log-argument" /* AppError.INVALID_LOG_ARGUMENT */]: 'First argument to `onLog` must be null or a function.',
|
|
["idb-open" /* AppError.IDB_OPEN */]: 'Error thrown when opening IndexedDB. Original error: {$originalErrorMessage}.',
|
|
["idb-get" /* AppError.IDB_GET */]: 'Error thrown when reading from IndexedDB. Original error: {$originalErrorMessage}.',
|
|
["idb-set" /* AppError.IDB_WRITE */]: 'Error thrown when writing to IndexedDB. Original error: {$originalErrorMessage}.',
|
|
["idb-delete" /* AppError.IDB_DELETE */]: 'Error thrown when deleting from IndexedDB. Original error: {$originalErrorMessage}.'
|
|
};
|
|
const ERROR_FACTORY = new ErrorFactory('app', 'Firebase', ERRORS);
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
class FirebaseAppImpl {
|
|
constructor(options, config, container) {
|
|
this._isDeleted = false;
|
|
this._options = Object.assign({}, options);
|
|
this._config = Object.assign({}, config);
|
|
this._name = config.name;
|
|
this._automaticDataCollectionEnabled =
|
|
config.automaticDataCollectionEnabled;
|
|
this._container = container;
|
|
this.container.addComponent(new Component('app', () => this, "PUBLIC" /* ComponentType.PUBLIC */));
|
|
}
|
|
get automaticDataCollectionEnabled() {
|
|
this.checkDestroyed();
|
|
return this._automaticDataCollectionEnabled;
|
|
}
|
|
set automaticDataCollectionEnabled(val) {
|
|
this.checkDestroyed();
|
|
this._automaticDataCollectionEnabled = val;
|
|
}
|
|
get name() {
|
|
this.checkDestroyed();
|
|
return this._name;
|
|
}
|
|
get options() {
|
|
this.checkDestroyed();
|
|
return this._options;
|
|
}
|
|
get config() {
|
|
this.checkDestroyed();
|
|
return this._config;
|
|
}
|
|
get container() {
|
|
return this._container;
|
|
}
|
|
get isDeleted() {
|
|
return this._isDeleted;
|
|
}
|
|
set isDeleted(val) {
|
|
this._isDeleted = val;
|
|
}
|
|
/**
|
|
* This function will throw an Error if the App has already been deleted -
|
|
* use before performing API actions on the App.
|
|
*/
|
|
checkDestroyed() {
|
|
if (this.isDeleted) {
|
|
throw ERROR_FACTORY.create("app-deleted" /* AppError.APP_DELETED */, { appName: this._name });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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 current SDK version.
|
|
*
|
|
* @public
|
|
*/
|
|
const SDK_VERSION = version;
|
|
function initializeApp(_options, rawConfig = {}) {
|
|
let options = _options;
|
|
if (typeof rawConfig !== 'object') {
|
|
const name = rawConfig;
|
|
rawConfig = { name };
|
|
}
|
|
const config = Object.assign({ name: DEFAULT_ENTRY_NAME, automaticDataCollectionEnabled: false }, rawConfig);
|
|
const name = config.name;
|
|
if (typeof name !== 'string' || !name) {
|
|
throw ERROR_FACTORY.create("bad-app-name" /* AppError.BAD_APP_NAME */, {
|
|
appName: String(name)
|
|
});
|
|
}
|
|
options || (options = getDefaultAppConfig());
|
|
if (!options) {
|
|
throw ERROR_FACTORY.create("no-options" /* AppError.NO_OPTIONS */);
|
|
}
|
|
const existingApp = _apps.get(name);
|
|
if (existingApp) {
|
|
// return the existing app if options and config deep equal the ones in the existing app.
|
|
if (deepEqual(options, existingApp.options) &&
|
|
deepEqual(config, existingApp.config)) {
|
|
return existingApp;
|
|
}
|
|
else {
|
|
throw ERROR_FACTORY.create("duplicate-app" /* AppError.DUPLICATE_APP */, { appName: name });
|
|
}
|
|
}
|
|
const container = new ComponentContainer(name);
|
|
for (const component of _components.values()) {
|
|
container.addComponent(component);
|
|
}
|
|
const newApp = new FirebaseAppImpl(options, config, container);
|
|
_apps.set(name, newApp);
|
|
return newApp;
|
|
}
|
|
/**
|
|
* Retrieves a {@link @firebase/app#FirebaseApp} instance.
|
|
*
|
|
* When called with no arguments, the default app is returned. When an app name
|
|
* is provided, the app corresponding to that name is returned.
|
|
*
|
|
* An exception is thrown if the app being retrieved has not yet been
|
|
* initialized.
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* // Return the default app
|
|
* const app = getApp();
|
|
* ```
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* // Return a named app
|
|
* const otherApp = getApp("otherApp");
|
|
* ```
|
|
*
|
|
* @param name - Optional name of the app to return. If no name is
|
|
* provided, the default is `"[DEFAULT]"`.
|
|
*
|
|
* @returns The app corresponding to the provided app name.
|
|
* If no app name is provided, the default app is returned.
|
|
*
|
|
* @public
|
|
*/
|
|
function getApp(name = DEFAULT_ENTRY_NAME) {
|
|
const app = _apps.get(name);
|
|
if (!app && name === DEFAULT_ENTRY_NAME && getDefaultAppConfig()) {
|
|
return initializeApp();
|
|
}
|
|
if (!app) {
|
|
throw ERROR_FACTORY.create("no-app" /* AppError.NO_APP */, { appName: name });
|
|
}
|
|
return app;
|
|
}
|
|
/**
|
|
* A (read-only) array of all initialized apps.
|
|
* @public
|
|
*/
|
|
function getApps() {
|
|
return Array.from(_apps.values());
|
|
}
|
|
/**
|
|
* Renders this app unusable and frees the resources of all associated
|
|
* services.
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* deleteApp(app)
|
|
* .then(function() {
|
|
* console.log("App deleted successfully");
|
|
* })
|
|
* .catch(function(error) {
|
|
* console.log("Error deleting app:", error);
|
|
* });
|
|
* ```
|
|
*
|
|
* @public
|
|
*/
|
|
async function deleteApp(app) {
|
|
const name = app.name;
|
|
if (_apps.has(name)) {
|
|
_apps.delete(name);
|
|
await Promise.all(app.container
|
|
.getProviders()
|
|
.map(provider => provider.delete()));
|
|
app.isDeleted = true;
|
|
}
|
|
}
|
|
/**
|
|
* Registers a library's name and version for platform logging purposes.
|
|
* @param library - Name of 1p or 3p library (e.g. firestore, angularfire)
|
|
* @param version - Current version of that library.
|
|
* @param variant - Bundle variant, e.g., node, rn, etc.
|
|
*
|
|
* @public
|
|
*/
|
|
function registerVersion(libraryKeyOrName, version, variant) {
|
|
var _a;
|
|
// TODO: We can use this check to whitelist strings when/if we set up
|
|
// a good whitelist system.
|
|
let library = (_a = PLATFORM_LOG_STRING[libraryKeyOrName]) !== null && _a !== void 0 ? _a : libraryKeyOrName;
|
|
if (variant) {
|
|
library += `-${variant}`;
|
|
}
|
|
const libraryMismatch = library.match(/\s|\//);
|
|
const versionMismatch = version.match(/\s|\//);
|
|
if (libraryMismatch || versionMismatch) {
|
|
const warning = [
|
|
`Unable to register library "${library}" with version "${version}":`
|
|
];
|
|
if (libraryMismatch) {
|
|
warning.push(`library name "${library}" contains illegal characters (whitespace or "/")`);
|
|
}
|
|
if (libraryMismatch && versionMismatch) {
|
|
warning.push('and');
|
|
}
|
|
if (versionMismatch) {
|
|
warning.push(`version name "${version}" contains illegal characters (whitespace or "/")`);
|
|
}
|
|
logger.warn(warning.join(' '));
|
|
return;
|
|
}
|
|
_registerComponent(new Component(`${library}-version`, () => ({ library, version }), "VERSION" /* ComponentType.VERSION */));
|
|
}
|
|
/**
|
|
* Sets log handler for all Firebase SDKs.
|
|
* @param logCallback - An optional custom log handler that executes user code whenever
|
|
* the Firebase SDK makes a logging call.
|
|
*
|
|
* @public
|
|
*/
|
|
function onLog(logCallback, options) {
|
|
if (logCallback !== null && typeof logCallback !== 'function') {
|
|
throw ERROR_FACTORY.create("invalid-log-argument" /* AppError.INVALID_LOG_ARGUMENT */);
|
|
}
|
|
setUserLogHandler(logCallback, options);
|
|
}
|
|
/**
|
|
* Sets log level for all Firebase SDKs.
|
|
*
|
|
* All of the log types above the current log level are captured (i.e. if
|
|
* you set the log level to `info`, errors are logged, but `debug` and
|
|
* `verbose` logs are not).
|
|
*
|
|
* @public
|
|
*/
|
|
function setLogLevel(logLevel) {
|
|
setLogLevel$1(logLevel);
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
const DB_NAME = 'firebase-heartbeat-database';
|
|
const DB_VERSION = 1;
|
|
const STORE_NAME = 'firebase-heartbeat-store';
|
|
let dbPromise = null;
|
|
function getDbPromise() {
|
|
if (!dbPromise) {
|
|
dbPromise = openDB(DB_NAME, DB_VERSION, {
|
|
upgrade: (db, oldVersion) => {
|
|
// 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 (oldVersion) {
|
|
case 0:
|
|
try {
|
|
db.createObjectStore(STORE_NAME);
|
|
}
|
|
catch (e) {
|
|
// Safari/iOS browsers throw occasional exceptions on
|
|
// db.createObjectStore() that may be a bug. Avoid blocking
|
|
// the rest of the app functionality.
|
|
console.warn(e);
|
|
}
|
|
}
|
|
}
|
|
}).catch(e => {
|
|
throw ERROR_FACTORY.create("idb-open" /* AppError.IDB_OPEN */, {
|
|
originalErrorMessage: e.message
|
|
});
|
|
});
|
|
}
|
|
return dbPromise;
|
|
}
|
|
async function readHeartbeatsFromIndexedDB(app) {
|
|
try {
|
|
const db = await getDbPromise();
|
|
const tx = db.transaction(STORE_NAME);
|
|
const result = await tx.objectStore(STORE_NAME).get(computeKey(app));
|
|
// We already have the value but tx.done can throw,
|
|
// so we need to await it here to catch errors
|
|
await tx.done;
|
|
return result;
|
|
}
|
|
catch (e) {
|
|
if (e instanceof FirebaseError) {
|
|
logger.warn(e.message);
|
|
}
|
|
else {
|
|
const idbGetError = ERROR_FACTORY.create("idb-get" /* AppError.IDB_GET */, {
|
|
originalErrorMessage: e === null || e === void 0 ? void 0 : e.message
|
|
});
|
|
logger.warn(idbGetError.message);
|
|
}
|
|
}
|
|
}
|
|
async function writeHeartbeatsToIndexedDB(app, heartbeatObject) {
|
|
try {
|
|
const db = await getDbPromise();
|
|
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
const objectStore = tx.objectStore(STORE_NAME);
|
|
await objectStore.put(heartbeatObject, computeKey(app));
|
|
await tx.done;
|
|
}
|
|
catch (e) {
|
|
if (e instanceof FirebaseError) {
|
|
logger.warn(e.message);
|
|
}
|
|
else {
|
|
const idbGetError = ERROR_FACTORY.create("idb-set" /* AppError.IDB_WRITE */, {
|
|
originalErrorMessage: e === null || e === void 0 ? void 0 : e.message
|
|
});
|
|
logger.warn(idbGetError.message);
|
|
}
|
|
}
|
|
}
|
|
function computeKey(app) {
|
|
return `${app.name}!${app.options.appId}`;
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2021 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
const MAX_HEADER_BYTES = 1024;
|
|
// 30 days
|
|
const STORED_HEARTBEAT_RETENTION_MAX_MILLIS = 30 * 24 * 60 * 60 * 1000;
|
|
class HeartbeatServiceImpl {
|
|
constructor(container) {
|
|
this.container = container;
|
|
/**
|
|
* In-memory cache for heartbeats, used by getHeartbeatsHeader() to generate
|
|
* the header string.
|
|
* Stores one record per date. This will be consolidated into the standard
|
|
* format of one record per user agent string before being sent as a header.
|
|
* Populated from indexedDB when the controller is instantiated and should
|
|
* be kept in sync with indexedDB.
|
|
* Leave public for easier testing.
|
|
*/
|
|
this._heartbeatsCache = null;
|
|
const app = this.container.getProvider('app').getImmediate();
|
|
this._storage = new HeartbeatStorageImpl(app);
|
|
this._heartbeatsCachePromise = this._storage.read().then(result => {
|
|
this._heartbeatsCache = result;
|
|
return result;
|
|
});
|
|
}
|
|
/**
|
|
* Called to report a heartbeat. The function will generate
|
|
* a HeartbeatsByUserAgent object, update heartbeatsCache, and persist it
|
|
* to IndexedDB.
|
|
* Note that we only store one heartbeat per day. So if a heartbeat for today is
|
|
* already logged, subsequent calls to this function in the same day will be ignored.
|
|
*/
|
|
async triggerHeartbeat() {
|
|
var _a, _b;
|
|
const platformLogger = this.container
|
|
.getProvider('platform-logger')
|
|
.getImmediate();
|
|
// This is the "Firebase user agent" string from the platform logger
|
|
// service, not the browser user agent.
|
|
const agent = platformLogger.getPlatformInfoString();
|
|
const date = getUTCDateString();
|
|
if (((_a = this._heartbeatsCache) === null || _a === void 0 ? void 0 : _a.heartbeats) == null) {
|
|
this._heartbeatsCache = await this._heartbeatsCachePromise;
|
|
// If we failed to construct a heartbeats cache, then return immediately.
|
|
if (((_b = this._heartbeatsCache) === null || _b === void 0 ? void 0 : _b.heartbeats) == null) {
|
|
return;
|
|
}
|
|
}
|
|
// Do not store a heartbeat if one is already stored for this day
|
|
// or if a header has already been sent today.
|
|
if (this._heartbeatsCache.lastSentHeartbeatDate === date ||
|
|
this._heartbeatsCache.heartbeats.some(singleDateHeartbeat => singleDateHeartbeat.date === date)) {
|
|
return;
|
|
}
|
|
else {
|
|
// There is no entry for this date. Create one.
|
|
this._heartbeatsCache.heartbeats.push({ date, agent });
|
|
}
|
|
// Remove entries older than 30 days.
|
|
this._heartbeatsCache.heartbeats = this._heartbeatsCache.heartbeats.filter(singleDateHeartbeat => {
|
|
const hbTimestamp = new Date(singleDateHeartbeat.date).valueOf();
|
|
const now = Date.now();
|
|
return now - hbTimestamp <= STORED_HEARTBEAT_RETENTION_MAX_MILLIS;
|
|
});
|
|
return this._storage.overwrite(this._heartbeatsCache);
|
|
}
|
|
/**
|
|
* Returns a base64 encoded string which can be attached to the heartbeat-specific header directly.
|
|
* It also clears all heartbeats from memory as well as in IndexedDB.
|
|
*
|
|
* NOTE: Consuming product SDKs should not send the header if this method
|
|
* returns an empty string.
|
|
*/
|
|
async getHeartbeatsHeader() {
|
|
var _a;
|
|
if (this._heartbeatsCache === null) {
|
|
await this._heartbeatsCachePromise;
|
|
}
|
|
// If it's still null or the array is empty, there is no data to send.
|
|
if (((_a = this._heartbeatsCache) === null || _a === void 0 ? void 0 : _a.heartbeats) == null ||
|
|
this._heartbeatsCache.heartbeats.length === 0) {
|
|
return '';
|
|
}
|
|
const date = getUTCDateString();
|
|
// Extract as many heartbeats from the cache as will fit under the size limit.
|
|
const { heartbeatsToSend, unsentEntries } = extractHeartbeatsForHeader(this._heartbeatsCache.heartbeats);
|
|
const headerString = base64urlEncodeWithoutPadding(JSON.stringify({ version: 2, heartbeats: heartbeatsToSend }));
|
|
// Store last sent date to prevent another being logged/sent for the same day.
|
|
this._heartbeatsCache.lastSentHeartbeatDate = date;
|
|
if (unsentEntries.length > 0) {
|
|
// Store any unsent entries if they exist.
|
|
this._heartbeatsCache.heartbeats = unsentEntries;
|
|
// This seems more likely than emptying the array (below) to lead to some odd state
|
|
// since the cache isn't empty and this will be called again on the next request,
|
|
// and is probably safest if we await it.
|
|
await this._storage.overwrite(this._heartbeatsCache);
|
|
}
|
|
else {
|
|
this._heartbeatsCache.heartbeats = [];
|
|
// Do not wait for this, to reduce latency.
|
|
void this._storage.overwrite(this._heartbeatsCache);
|
|
}
|
|
return headerString;
|
|
}
|
|
}
|
|
function getUTCDateString() {
|
|
const today = new Date();
|
|
// Returns date format 'YYYY-MM-DD'
|
|
return today.toISOString().substring(0, 10);
|
|
}
|
|
function extractHeartbeatsForHeader(heartbeatsCache, maxSize = MAX_HEADER_BYTES) {
|
|
// Heartbeats grouped by user agent in the standard format to be sent in
|
|
// the header.
|
|
const heartbeatsToSend = [];
|
|
// Single date format heartbeats that are not sent.
|
|
let unsentEntries = heartbeatsCache.slice();
|
|
for (const singleDateHeartbeat of heartbeatsCache) {
|
|
// Look for an existing entry with the same user agent.
|
|
const heartbeatEntry = heartbeatsToSend.find(hb => hb.agent === singleDateHeartbeat.agent);
|
|
if (!heartbeatEntry) {
|
|
// If no entry for this user agent exists, create one.
|
|
heartbeatsToSend.push({
|
|
agent: singleDateHeartbeat.agent,
|
|
dates: [singleDateHeartbeat.date]
|
|
});
|
|
if (countBytes(heartbeatsToSend) > maxSize) {
|
|
// If the header would exceed max size, remove the added heartbeat
|
|
// entry and stop adding to the header.
|
|
heartbeatsToSend.pop();
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
heartbeatEntry.dates.push(singleDateHeartbeat.date);
|
|
// If the header would exceed max size, remove the added date
|
|
// and stop adding to the header.
|
|
if (countBytes(heartbeatsToSend) > maxSize) {
|
|
heartbeatEntry.dates.pop();
|
|
break;
|
|
}
|
|
}
|
|
// Pop unsent entry from queue. (Skipped if adding the entry exceeded
|
|
// quota and the loop breaks early.)
|
|
unsentEntries = unsentEntries.slice(1);
|
|
}
|
|
return {
|
|
heartbeatsToSend,
|
|
unsentEntries
|
|
};
|
|
}
|
|
class HeartbeatStorageImpl {
|
|
constructor(app) {
|
|
this.app = app;
|
|
this._canUseIndexedDBPromise = this.runIndexedDBEnvironmentCheck();
|
|
}
|
|
async runIndexedDBEnvironmentCheck() {
|
|
if (!isIndexedDBAvailable()) {
|
|
return false;
|
|
}
|
|
else {
|
|
return validateIndexedDBOpenable()
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
}
|
|
}
|
|
/**
|
|
* Read all heartbeats.
|
|
*/
|
|
async read() {
|
|
const canUseIndexedDB = await this._canUseIndexedDBPromise;
|
|
if (!canUseIndexedDB) {
|
|
return { heartbeats: [] };
|
|
}
|
|
else {
|
|
const idbHeartbeatObject = await readHeartbeatsFromIndexedDB(this.app);
|
|
if (idbHeartbeatObject === null || idbHeartbeatObject === void 0 ? void 0 : idbHeartbeatObject.heartbeats) {
|
|
return idbHeartbeatObject;
|
|
}
|
|
else {
|
|
return { heartbeats: [] };
|
|
}
|
|
}
|
|
}
|
|
// overwrite the storage with the provided heartbeats
|
|
async overwrite(heartbeatsObject) {
|
|
var _a;
|
|
const canUseIndexedDB = await this._canUseIndexedDBPromise;
|
|
if (!canUseIndexedDB) {
|
|
return;
|
|
}
|
|
else {
|
|
const existingHeartbeatsObject = await this.read();
|
|
return writeHeartbeatsToIndexedDB(this.app, {
|
|
lastSentHeartbeatDate: (_a = heartbeatsObject.lastSentHeartbeatDate) !== null && _a !== void 0 ? _a : existingHeartbeatsObject.lastSentHeartbeatDate,
|
|
heartbeats: heartbeatsObject.heartbeats
|
|
});
|
|
}
|
|
}
|
|
// add heartbeats
|
|
async add(heartbeatsObject) {
|
|
var _a;
|
|
const canUseIndexedDB = await this._canUseIndexedDBPromise;
|
|
if (!canUseIndexedDB) {
|
|
return;
|
|
}
|
|
else {
|
|
const existingHeartbeatsObject = await this.read();
|
|
return writeHeartbeatsToIndexedDB(this.app, {
|
|
lastSentHeartbeatDate: (_a = heartbeatsObject.lastSentHeartbeatDate) !== null && _a !== void 0 ? _a : existingHeartbeatsObject.lastSentHeartbeatDate,
|
|
heartbeats: [
|
|
...existingHeartbeatsObject.heartbeats,
|
|
...heartbeatsObject.heartbeats
|
|
]
|
|
});
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Calculate bytes of a HeartbeatsByUserAgent array after being wrapped
|
|
* in a platform logging header JSON object, stringified, and converted
|
|
* to base 64.
|
|
*/
|
|
function countBytes(heartbeatsCache) {
|
|
// base64 has a restricted set of characters, all of which should be 1 byte.
|
|
return base64urlEncodeWithoutPadding(
|
|
// heartbeatsCache wrapper properties
|
|
JSON.stringify({ version: 2, heartbeats: heartbeatsCache })).length;
|
|
}
|
|
|
|
/**
|
|
* @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 registerCoreComponents(variant) {
|
|
_registerComponent(new Component('platform-logger', container => new PlatformLoggerServiceImpl(container), "PRIVATE" /* ComponentType.PRIVATE */));
|
|
_registerComponent(new Component('heartbeat', container => new HeartbeatServiceImpl(container), "PRIVATE" /* ComponentType.PRIVATE */));
|
|
// Register `app` package.
|
|
registerVersion(name$o, version$1, variant);
|
|
// BUILD_TARGET will be replaced by values like esm5, esm2017, cjs5, etc during the compilation
|
|
registerVersion(name$o, version$1, 'esm2017');
|
|
// Register platform SDK identifier (no version).
|
|
registerVersion('fire-js', '');
|
|
}
|
|
|
|
/**
|
|
* Firebase App
|
|
*
|
|
* @remarks This package coordinates the communication between the different Firebase components
|
|
* @packageDocumentation
|
|
*/
|
|
registerCoreComponents('');
|
|
|
|
export { SDK_VERSION, DEFAULT_ENTRY_NAME as _DEFAULT_ENTRY_NAME, _addComponent, _addOrOverwriteComponent, _apps, _clearComponents, _components, _getProvider, _registerComponent, _removeServiceInstance, deleteApp, getApp, getApps, initializeApp, onLog, registerVersion, setLogLevel };
|
|
//# sourceMappingURL=index.esm2017.js.map
|