summaryrefslogtreecommitdiff
path: root/frontend-old/node_modules/@firebase/remote-config/dist/index.cjs.js
diff options
context:
space:
mode:
Diffstat (limited to 'frontend-old/node_modules/@firebase/remote-config/dist/index.cjs.js')
-rw-r--r--frontend-old/node_modules/@firebase/remote-config/dist/index.cjs.js2137
1 files changed, 2137 insertions, 0 deletions
diff --git a/frontend-old/node_modules/@firebase/remote-config/dist/index.cjs.js b/frontend-old/node_modules/@firebase/remote-config/dist/index.cjs.js
new file mode 100644
index 0000000..dacd9fd
--- /dev/null
+++ b/frontend-old/node_modules/@firebase/remote-config/dist/index.cjs.js
@@ -0,0 +1,2137 @@
+'use strict';
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+var app = require('@firebase/app');
+var util = require('@firebase/util');
+var component = require('@firebase/component');
+var logger = require('@firebase/logger');
+require('@firebase/installations');
+
+const name = "@firebase/remote-config";
+const version = "0.7.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.
+ */
+/**
+ * Shims a minimal AbortSignal.
+ *
+ * <p>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.
+ */
+class RemoteConfigAbortSignal {
+ constructor() {
+ this.listeners = [];
+ }
+ addEventListener(listener) {
+ this.listeners.push(listener);
+ }
+ abort() {
+ this.listeners.forEach(listener => listener());
+ }
+}
+
+/**
+ * @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.
+ */
+const RC_COMPONENT_NAME = 'remote-config';
+const RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 100;
+const RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH = 250;
+const RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH = 500;
+
+/**
+ * @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 ERROR_DESCRIPTION_MAP = {
+ ["already-initialized" /* ErrorCode.ALREADY_INITIALIZED */]: 'Remote Config already initialized',
+ ["registration-window" /* ErrorCode.REGISTRATION_WINDOW */]: 'Undefined window object. This SDK only supports usage in a browser environment.',
+ ["registration-project-id" /* ErrorCode.REGISTRATION_PROJECT_ID */]: 'Undefined project identifier. Check Firebase app initialization.',
+ ["registration-api-key" /* ErrorCode.REGISTRATION_API_KEY */]: 'Undefined API key. Check Firebase app initialization.',
+ ["registration-app-id" /* ErrorCode.REGISTRATION_APP_ID */]: 'Undefined app identifier. Check Firebase app initialization.',
+ ["storage-open" /* ErrorCode.STORAGE_OPEN */]: 'Error thrown when opening storage. Original error: {$originalErrorMessage}.',
+ ["storage-get" /* ErrorCode.STORAGE_GET */]: 'Error thrown when reading from storage. Original error: {$originalErrorMessage}.',
+ ["storage-set" /* ErrorCode.STORAGE_SET */]: 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.',
+ ["storage-delete" /* ErrorCode.STORAGE_DELETE */]: 'Error thrown when deleting from storage. Original error: {$originalErrorMessage}.',
+ ["fetch-client-network" /* ErrorCode.FETCH_NETWORK */]: 'Fetch client failed to connect to a network. Check Internet connection.' +
+ ' Original error: {$originalErrorMessage}.',
+ ["fetch-timeout" /* ErrorCode.FETCH_TIMEOUT */]: 'The config fetch request timed out. ' +
+ ' Configure timeout using "fetchTimeoutMillis" SDK setting.',
+ ["fetch-throttle" /* ErrorCode.FETCH_THROTTLE */]: 'The config fetch request timed out while in an exponential backoff state.' +
+ ' Configure timeout using "fetchTimeoutMillis" SDK setting.' +
+ ' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.',
+ ["fetch-client-parse" /* ErrorCode.FETCH_PARSE */]: 'Fetch client could not parse response.' +
+ ' Original error: {$originalErrorMessage}.',
+ ["fetch-status" /* ErrorCode.FETCH_STATUS */]: 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.',
+ ["indexed-db-unavailable" /* ErrorCode.INDEXED_DB_UNAVAILABLE */]: 'Indexed DB is not supported by current browser',
+ ["custom-signal-max-allowed-signals" /* ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS */]: 'Setting more than {$maxSignals} custom signals is not supported.',
+ ["stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */]: 'The stream was not able to connect to the backend: {$originalErrorMessage}.',
+ ["realtime-unavailable" /* ErrorCode.CONFIG_UPDATE_UNAVAILABLE */]: 'The Realtime service is unavailable: {$originalErrorMessage}',
+ ["update-message-invalid" /* ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID */]: 'The stream invalidation message was unparsable: {$originalErrorMessage}',
+ ["update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */]: 'Unable to fetch the latest config: {$originalErrorMessage}'
+};
+const ERROR_FACTORY = new util.ErrorFactory('remoteconfig' /* service */, 'Remote Config' /* service name */, ERROR_DESCRIPTION_MAP);
+// Note how this is like typeof/instanceof, but for ErrorCode.
+function hasErrorCode(e, errorCode) {
+ return e instanceof util.FirebaseError && e.code.indexOf(errorCode) !== -1;
+}
+
+/**
+ * @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 DEFAULT_VALUE_FOR_BOOLEAN = false;
+const DEFAULT_VALUE_FOR_STRING = '';
+const DEFAULT_VALUE_FOR_NUMBER = 0;
+const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
+class Value {
+ constructor(_source, _value = DEFAULT_VALUE_FOR_STRING) {
+ this._source = _source;
+ this._value = _value;
+ }
+ asString() {
+ return this._value;
+ }
+ asBoolean() {
+ if (this._source === 'static') {
+ return DEFAULT_VALUE_FOR_BOOLEAN;
+ }
+ return BOOLEAN_TRUTHY_VALUES.indexOf(this._value.toLowerCase()) >= 0;
+ }
+ asNumber() {
+ if (this._source === 'static') {
+ return DEFAULT_VALUE_FOR_NUMBER;
+ }
+ let num = Number(this._value);
+ if (isNaN(num)) {
+ num = DEFAULT_VALUE_FOR_NUMBER;
+ }
+ return num;
+ }
+ getSource() {
+ return this._source;
+ }
+}
+
+/**
+ * @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.
+ */
+/**
+ *
+ * @param app - The {@link @firebase/app#FirebaseApp} instance.
+ * @param options - Optional. The {@link RemoteConfigOptions} with which to instantiate the
+ * Remote Config instance.
+ * @returns A {@link RemoteConfig} instance.
+ *
+ * @public
+ */
+function getRemoteConfig(app$1 = app.getApp(), options = {}) {
+ app$1 = util.getModularInstance(app$1);
+ const rcProvider = app._getProvider(app$1, RC_COMPONENT_NAME);
+ if (rcProvider.isInitialized()) {
+ const initialOptions = rcProvider.getOptions();
+ if (util.deepEqual(initialOptions, options)) {
+ return rcProvider.getImmediate();
+ }
+ throw ERROR_FACTORY.create("already-initialized" /* ErrorCode.ALREADY_INITIALIZED */);
+ }
+ rcProvider.initialize({ options });
+ const rc = rcProvider.getImmediate();
+ if (options.initialFetchResponse) {
+ // We use these initial writes as the initialization promise since they will hydrate the same
+ // fields that `storageCache.loadFromStorage` would set.
+ rc._initializePromise = Promise.all([
+ rc._storage.setLastSuccessfulFetchResponse(options.initialFetchResponse),
+ rc._storage.setActiveConfigEtag(options.initialFetchResponse?.eTag || ''),
+ rc._storage.setActiveConfigTemplateVersion(options.initialFetchResponse.templateVersion || 0),
+ rc._storageCache.setLastSuccessfulFetchTimestampMillis(Date.now()),
+ rc._storageCache.setLastFetchStatus('success'),
+ rc._storageCache.setActiveConfig(options.initialFetchResponse?.config || {})
+ ]).then();
+ // The `storageCache` methods above set their in-memory fields synchronously, so it's
+ // safe to declare our initialization complete at this point.
+ rc._isInitializationComplete = true;
+ }
+ return rc;
+}
+/**
+ * Makes the last fetched config available to the getters.
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @returns A `Promise` which resolves to true if the current call activated the fetched configs.
+ * If the fetched configs were already activated, the `Promise` will resolve to false.
+ *
+ * @public
+ */
+async function activate(remoteConfig) {
+ const rc = util.getModularInstance(remoteConfig);
+ const [lastSuccessfulFetchResponse, activeConfigEtag] = await Promise.all([
+ rc._storage.getLastSuccessfulFetchResponse(),
+ rc._storage.getActiveConfigEtag()
+ ]);
+ if (!lastSuccessfulFetchResponse ||
+ !lastSuccessfulFetchResponse.config ||
+ !lastSuccessfulFetchResponse.eTag ||
+ !lastSuccessfulFetchResponse.templateVersion ||
+ lastSuccessfulFetchResponse.eTag === activeConfigEtag) {
+ // Either there is no successful fetched config, or is the same as current active
+ // config.
+ return false;
+ }
+ await Promise.all([
+ rc._storageCache.setActiveConfig(lastSuccessfulFetchResponse.config),
+ rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag),
+ rc._storage.setActiveConfigTemplateVersion(lastSuccessfulFetchResponse.templateVersion)
+ ]);
+ return true;
+}
+/**
+ * Ensures the last activated config are available to the getters.
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ *
+ * @returns A `Promise` that resolves when the last activated config is available to the getters.
+ * @public
+ */
+function ensureInitialized(remoteConfig) {
+ const rc = util.getModularInstance(remoteConfig);
+ if (!rc._initializePromise) {
+ rc._initializePromise = rc._storageCache.loadFromStorage().then(() => {
+ rc._isInitializationComplete = true;
+ });
+ }
+ return rc._initializePromise;
+}
+/**
+ * Fetches and caches configuration from the Remote Config service.
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @public
+ */
+async function fetchConfig(remoteConfig) {
+ const rc = util.getModularInstance(remoteConfig);
+ // Aborts the request after the given timeout, causing the fetch call to
+ // reject with an `AbortError`.
+ //
+ // <p>Aborting after the request completes is a no-op, so we don't need a
+ // corresponding `clearTimeout`.
+ //
+ // Locating abort logic here because:
+ // * it uses a developer setting (timeout)
+ // * it applies to all retries (like curl's max-time arg)
+ // * it is consistent with the Fetch API's signal input
+ const abortSignal = new RemoteConfigAbortSignal();
+ setTimeout(async () => {
+ // Note a very low delay, eg < 10ms, can elapse before listeners are initialized.
+ abortSignal.abort();
+ }, rc.settings.fetchTimeoutMillis);
+ const customSignals = rc._storageCache.getCustomSignals();
+ if (customSignals) {
+ rc._logger.debug(`Fetching config with custom signals: ${JSON.stringify(customSignals)}`);
+ }
+ // Catches *all* errors thrown by client so status can be set consistently.
+ try {
+ await rc._client.fetch({
+ cacheMaxAgeMillis: rc.settings.minimumFetchIntervalMillis,
+ signal: abortSignal,
+ customSignals
+ });
+ await rc._storageCache.setLastFetchStatus('success');
+ }
+ catch (e) {
+ const lastFetchStatus = hasErrorCode(e, "fetch-throttle" /* ErrorCode.FETCH_THROTTLE */)
+ ? 'throttle'
+ : 'failure';
+ await rc._storageCache.setLastFetchStatus(lastFetchStatus);
+ throw e;
+ }
+}
+/**
+ * Gets all config.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @returns All config.
+ *
+ * @public
+ */
+function getAll(remoteConfig) {
+ const rc = util.getModularInstance(remoteConfig);
+ return getAllKeys(rc._storageCache.getActiveConfig(), rc.defaultConfig).reduce((allConfigs, key) => {
+ allConfigs[key] = getValue(remoteConfig, key);
+ return allConfigs;
+ }, {});
+}
+/**
+ * Gets the value for the given key as a boolean.
+ *
+ * Convenience method for calling <code>remoteConfig.getValue(key).asBoolean()</code>.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @param key - The name of the parameter.
+ *
+ * @returns The value for the given key as a boolean.
+ * @public
+ */
+function getBoolean(remoteConfig, key) {
+ return getValue(util.getModularInstance(remoteConfig), key).asBoolean();
+}
+/**
+ * Gets the value for the given key as a number.
+ *
+ * Convenience method for calling <code>remoteConfig.getValue(key).asNumber()</code>.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @param key - The name of the parameter.
+ *
+ * @returns The value for the given key as a number.
+ *
+ * @public
+ */
+function getNumber(remoteConfig, key) {
+ return getValue(util.getModularInstance(remoteConfig), key).asNumber();
+}
+/**
+ * Gets the value for the given key as a string.
+ * Convenience method for calling <code>remoteConfig.getValue(key).asString()</code>.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @param key - The name of the parameter.
+ *
+ * @returns The value for the given key as a string.
+ *
+ * @public
+ */
+function getString(remoteConfig, key) {
+ return getValue(util.getModularInstance(remoteConfig), key).asString();
+}
+/**
+ * Gets the {@link Value} for the given key.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @param key - The name of the parameter.
+ *
+ * @returns The value for the given key.
+ *
+ * @public
+ */
+function getValue(remoteConfig, key) {
+ const rc = util.getModularInstance(remoteConfig);
+ if (!rc._isInitializationComplete) {
+ rc._logger.debug(`A value was requested for key "${key}" before SDK initialization completed.` +
+ ' Await on ensureInitialized if the intent was to get a previously activated value.');
+ }
+ const activeConfig = rc._storageCache.getActiveConfig();
+ if (activeConfig && activeConfig[key] !== undefined) {
+ return new Value('remote', activeConfig[key]);
+ }
+ else if (rc.defaultConfig && rc.defaultConfig[key] !== undefined) {
+ return new Value('default', String(rc.defaultConfig[key]));
+ }
+ rc._logger.debug(`Returning static value for key "${key}".` +
+ ' Define a default or remote value if this is unintentional.');
+ return new Value('static');
+}
+/**
+ * Defines the log level to use.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @param logLevel - The log level to set.
+ *
+ * @public
+ */
+function setLogLevel(remoteConfig, logLevel) {
+ const rc = util.getModularInstance(remoteConfig);
+ switch (logLevel) {
+ case 'debug':
+ rc._logger.logLevel = logger.LogLevel.DEBUG;
+ break;
+ case 'silent':
+ rc._logger.logLevel = logger.LogLevel.SILENT;
+ break;
+ default:
+ rc._logger.logLevel = logger.LogLevel.ERROR;
+ }
+}
+/**
+ * Dedupes and returns an array of all the keys of the received objects.
+ */
+function getAllKeys(obj1 = {}, obj2 = {}) {
+ return Object.keys({ ...obj1, ...obj2 });
+}
+/**
+ * Sets the custom signals for the app instance.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @param customSignals - Map (key, value) of the custom signals to be set for the app instance. If
+ * a key already exists, the value is overwritten. Setting the value of a custom signal to null
+ * unsets the signal. The signals will be persisted locally on the client.
+ *
+ * @public
+ */
+async function setCustomSignals(remoteConfig, customSignals) {
+ const rc = util.getModularInstance(remoteConfig);
+ if (Object.keys(customSignals).length === 0) {
+ return;
+ }
+ // eslint-disable-next-line guard-for-in
+ for (const key in customSignals) {
+ if (key.length > RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH) {
+ rc._logger.error(`Custom signal key ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH}.`);
+ return;
+ }
+ const value = customSignals[key];
+ if (typeof value === 'string' &&
+ value.length > RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH) {
+ rc._logger.error(`Value supplied for custom signal ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH}.`);
+ return;
+ }
+ }
+ try {
+ await rc._storageCache.setCustomSignals(customSignals);
+ }
+ catch (error) {
+ rc._logger.error(`Error encountered while setting custom signals: ${error}`);
+ }
+}
+// TODO: Add public document for the Remote Config Realtime API guide on the Web Platform.
+/**
+ * Starts listening for real-time config updates from the Remote Config backend and automatically
+ * fetches updates from the Remote Config backend when they are available.
+ *
+ * @remarks
+ * If a connection to the Remote Config backend is not already open, calling this method will
+ * open it. Multiple listeners can be added by calling this method again, but subsequent calls
+ * re-use the same connection to the backend.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ * @param observer - The {@link ConfigUpdateObserver} to be notified of config updates.
+ * @returns An {@link Unsubscribe} function to remove the listener.
+ *
+ * @public
+ */
+function onConfigUpdate(remoteConfig, observer) {
+ const rc = util.getModularInstance(remoteConfig);
+ rc._realtimeHandler.addObserver(observer);
+ return () => {
+ rc._realtimeHandler.removeObserver(observer);
+ };
+}
+
+/**
+ * @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.
+ */
+/**
+ * Implements the {@link RemoteConfigClient} abstraction with success response caching.
+ *
+ * <p>Comparable to the browser's Cache API for responses, but the Cache API requires a Service
+ * Worker, which requires HTTPS, which would significantly complicate SDK installation. Also, the
+ * Cache API doesn't support matching entries by time.
+ */
+class CachingClient {
+ constructor(client, storage, storageCache, logger) {
+ this.client = client;
+ this.storage = storage;
+ this.storageCache = storageCache;
+ this.logger = logger;
+ }
+ /**
+ * Returns true if the age of the cached fetched configs is less than or equal to
+ * {@link Settings#minimumFetchIntervalInSeconds}.
+ *
+ * <p>This is comparable to passing `headers = { 'Cache-Control': max-age <maxAge> }` to the
+ * native Fetch API.
+ *
+ * <p>Visible for testing.
+ */
+ isCachedDataFresh(cacheMaxAgeMillis, lastSuccessfulFetchTimestampMillis) {
+ // Cache can only be fresh if it's populated.
+ if (!lastSuccessfulFetchTimestampMillis) {
+ this.logger.debug('Config fetch cache check. Cache unpopulated.');
+ return false;
+ }
+ // Calculates age of cache entry.
+ const cacheAgeMillis = Date.now() - lastSuccessfulFetchTimestampMillis;
+ const isCachedDataFresh = cacheAgeMillis <= cacheMaxAgeMillis;
+ this.logger.debug('Config fetch cache check.' +
+ ` Cache age millis: ${cacheAgeMillis}.` +
+ ` Cache max age millis (minimumFetchIntervalMillis setting): ${cacheMaxAgeMillis}.` +
+ ` Is cache hit: ${isCachedDataFresh}.`);
+ return isCachedDataFresh;
+ }
+ async fetch(request) {
+ // Reads from persisted storage to avoid cache miss if callers don't wait on initialization.
+ const [lastSuccessfulFetchTimestampMillis, lastSuccessfulFetchResponse] = await Promise.all([
+ this.storage.getLastSuccessfulFetchTimestampMillis(),
+ this.storage.getLastSuccessfulFetchResponse()
+ ]);
+ // Exits early on cache hit.
+ if (lastSuccessfulFetchResponse &&
+ this.isCachedDataFresh(request.cacheMaxAgeMillis, lastSuccessfulFetchTimestampMillis)) {
+ return lastSuccessfulFetchResponse;
+ }
+ // Deviates from pure decorator by not honoring a passed ETag since we don't have a public API
+ // that allows the caller to pass an ETag.
+ request.eTag =
+ lastSuccessfulFetchResponse && lastSuccessfulFetchResponse.eTag;
+ // Falls back to service on cache miss.
+ const response = await this.client.fetch(request);
+ // Fetch throws for non-success responses, so success is guaranteed here.
+ const storageOperations = [
+ // Uses write-through cache for consistency with synchronous public API.
+ this.storageCache.setLastSuccessfulFetchTimestampMillis(Date.now())
+ ];
+ if (response.status === 200) {
+ // Caches response only if it has changed, ie non-304 responses.
+ storageOperations.push(this.storage.setLastSuccessfulFetchResponse(response));
+ }
+ await Promise.all(storageOperations);
+ return response;
+ }
+}
+
+/**
+ * @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.
+ */
+/**
+ * Attempts to get the most accurate browser language setting.
+ *
+ * <p>Adapted from getUserLanguage in packages/auth/src/utils.js for TypeScript.
+ *
+ * <p>Defers default language specification to server logic for consistency.
+ *
+ * @param navigatorLanguage Enables tests to override read-only {@link NavigatorLanguage}.
+ */
+function getUserLanguage(navigatorLanguage = navigator) {
+ return (
+ // Most reliable, but only supported in Chrome/Firefox.
+ (navigatorLanguage.languages && navigatorLanguage.languages[0]) ||
+ // Supported in most browsers, but returns the language of the browser
+ // UI, not the language set in browser settings.
+ navigatorLanguage.language
+ // Polyfill otherwise.
+ );
+}
+
+/**
+ * @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.
+ */
+/**
+ * Implements the Client abstraction for the Remote Config REST API.
+ */
+class RestClient {
+ constructor(firebaseInstallations, sdkVersion, namespace, projectId, apiKey, appId) {
+ this.firebaseInstallations = firebaseInstallations;
+ this.sdkVersion = sdkVersion;
+ this.namespace = namespace;
+ this.projectId = projectId;
+ this.apiKey = apiKey;
+ this.appId = appId;
+ }
+ /**
+ * Fetches from the Remote Config REST API.
+ *
+ * @throws a {@link ErrorCode.FETCH_NETWORK} error if {@link GlobalFetch#fetch} can't
+ * connect to the network.
+ * @throws a {@link ErrorCode.FETCH_PARSE} error if {@link Response#json} can't parse the
+ * fetch response.
+ * @throws a {@link ErrorCode.FETCH_STATUS} error if the service returns an HTTP error status.
+ */
+ async fetch(request) {
+ const [installationId, installationToken] = await Promise.all([
+ this.firebaseInstallations.getId(),
+ this.firebaseInstallations.getToken()
+ ]);
+ const urlBase = window.FIREBASE_REMOTE_CONFIG_URL_BASE ||
+ 'https://firebaseremoteconfig.googleapis.com';
+ const url = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:fetch?key=${this.apiKey}`;
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'Content-Encoding': 'gzip',
+ // Deviates from pure decorator by not passing max-age header since we don't currently have
+ // service behavior using that header.
+ 'If-None-Match': request.eTag || '*'
+ // TODO: Add this header once CORS error is fixed internally.
+ //'X-Firebase-RC-Fetch-Type': `${fetchType}/${fetchAttempt}`
+ };
+ const requestBody = {
+ /* eslint-disable camelcase */
+ sdk_version: this.sdkVersion,
+ app_instance_id: installationId,
+ app_instance_id_token: installationToken,
+ app_id: this.appId,
+ language_code: getUserLanguage(),
+ custom_signals: request.customSignals
+ /* eslint-enable camelcase */
+ };
+ const options = {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(requestBody)
+ };
+ // This logic isn't REST-specific, but shimming abort logic isn't worth another decorator.
+ const fetchPromise = fetch(url, options);
+ const timeoutPromise = new Promise((_resolve, reject) => {
+ // Maps async event listener to Promise API.
+ request.signal.addEventListener(() => {
+ // Emulates https://heycam.github.io/webidl/#aborterror
+ const error = new Error('The operation was aborted.');
+ error.name = 'AbortError';
+ reject(error);
+ });
+ });
+ let response;
+ try {
+ await Promise.race([fetchPromise, timeoutPromise]);
+ response = await fetchPromise;
+ }
+ catch (originalError) {
+ let errorCode = "fetch-client-network" /* ErrorCode.FETCH_NETWORK */;
+ if (originalError?.name === 'AbortError') {
+ errorCode = "fetch-timeout" /* ErrorCode.FETCH_TIMEOUT */;
+ }
+ throw ERROR_FACTORY.create(errorCode, {
+ originalErrorMessage: originalError?.message
+ });
+ }
+ let status = response.status;
+ // Normalizes nullable header to optional.
+ const responseEtag = response.headers.get('ETag') || undefined;
+ let config;
+ let state;
+ let templateVersion;
+ // JSON parsing throws SyntaxError if the response body isn't a JSON string.
+ // Requesting application/json and checking for a 200 ensures there's JSON data.
+ if (response.status === 200) {
+ let responseBody;
+ try {
+ responseBody = await response.json();
+ }
+ catch (originalError) {
+ throw ERROR_FACTORY.create("fetch-client-parse" /* ErrorCode.FETCH_PARSE */, {
+ originalErrorMessage: originalError?.message
+ });
+ }
+ config = responseBody['entries'];
+ state = responseBody['state'];
+ templateVersion = responseBody['templateVersion'];
+ }
+ // Normalizes based on legacy state.
+ if (state === 'INSTANCE_STATE_UNSPECIFIED') {
+ status = 500;
+ }
+ else if (state === 'NO_CHANGE') {
+ status = 304;
+ }
+ else if (state === 'NO_TEMPLATE' || state === 'EMPTY_CONFIG') {
+ // These cases can be fixed remotely, so normalize to safe value.
+ config = {};
+ }
+ // Normalize to exception-based control flow for non-success cases.
+ // Encapsulates HTTP specifics in this class as much as possible. Status is still the best for
+ // differentiating success states (200 from 304; the state body param is undefined in a
+ // standard 304).
+ if (status !== 304 && status !== 200) {
+ throw ERROR_FACTORY.create("fetch-status" /* ErrorCode.FETCH_STATUS */, {
+ httpStatus: status
+ });
+ }
+ return { status, eTag: responseEtag, config, templateVersion };
+ }
+}
+
+/**
+ * @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.
+ */
+/**
+ * Supports waiting on a backoff by:
+ *
+ * <ul>
+ * <li>Promisifying setTimeout, so we can set a timeout in our Promise chain</li>
+ * <li>Listening on a signal bus for abort events, just like the Fetch API</li>
+ * <li>Failing in the same way the Fetch API fails, so timing out a live request and a throttled
+ * request appear the same.</li>
+ * </ul>
+ *
+ * <p>Visible for testing.
+ */
+function setAbortableTimeout(signal, throttleEndTimeMillis) {
+ return new Promise((resolve, reject) => {
+ // Derives backoff from given end time, normalizing negative numbers to zero.
+ const backoffMillis = Math.max(throttleEndTimeMillis - Date.now(), 0);
+ const timeout = setTimeout(resolve, backoffMillis);
+ // Adds listener, rather than sets onabort, because signal is a shared object.
+ signal.addEventListener(() => {
+ clearTimeout(timeout);
+ // If the request completes before this timeout, the rejection has no effect.
+ reject(ERROR_FACTORY.create("fetch-throttle" /* ErrorCode.FETCH_THROTTLE */, {
+ throttleEndTimeMillis
+ }));
+ });
+ });
+}
+/**
+ * Returns true if the {@link Error} indicates a fetch request may succeed later.
+ */
+function isRetriableError(e) {
+ if (!(e instanceof util.FirebaseError) || !e.customData) {
+ return false;
+ }
+ // Uses string index defined by ErrorData, which FirebaseError implements.
+ const httpStatus = Number(e.customData['httpStatus']);
+ return (httpStatus === 429 ||
+ httpStatus === 500 ||
+ httpStatus === 503 ||
+ httpStatus === 504);
+}
+/**
+ * Decorates a Client with retry logic.
+ *
+ * <p>Comparable to CachingClient, but uses backoff logic instead of cache max age and doesn't cache
+ * responses (because the SDK has no use for error responses).
+ */
+class RetryingClient {
+ constructor(client, storage) {
+ this.client = client;
+ this.storage = storage;
+ }
+ async fetch(request) {
+ const throttleMetadata = (await this.storage.getThrottleMetadata()) || {
+ backoffCount: 0,
+ throttleEndTimeMillis: Date.now()
+ };
+ return this.attemptFetch(request, throttleMetadata);
+ }
+ /**
+ * A recursive helper for attempting a fetch request repeatedly.
+ *
+ * @throws any non-retriable errors.
+ */
+ async attemptFetch(request, { throttleEndTimeMillis, backoffCount }) {
+ // Starts with a (potentially zero) timeout to support resumption from stored state.
+ // Ensures the throttle end time is honored if the last attempt timed out.
+ // Note the SDK will never make a request if the fetch timeout expires at this point.
+ await setAbortableTimeout(request.signal, throttleEndTimeMillis);
+ try {
+ const response = await this.client.fetch(request);
+ // Note the SDK only clears throttle state if response is success or non-retriable.
+ await this.storage.deleteThrottleMetadata();
+ return response;
+ }
+ catch (e) {
+ if (!isRetriableError(e)) {
+ throw e;
+ }
+ // Increments backoff state.
+ const throttleMetadata = {
+ throttleEndTimeMillis: Date.now() + util.calculateBackoffMillis(backoffCount),
+ backoffCount: backoffCount + 1
+ };
+ // Persists state.
+ await this.storage.setThrottleMetadata(throttleMetadata);
+ return this.attemptFetch(request, throttleMetadata);
+ }
+ }
+}
+
+/**
+ * @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 DEFAULT_FETCH_TIMEOUT_MILLIS = 60 * 1000; // One minute
+const DEFAULT_CACHE_MAX_AGE_MILLIS = 12 * 60 * 60 * 1000; // Twelve hours.
+/**
+ * Encapsulates business logic mapping network and storage dependencies to the public SDK API.
+ *
+ * See {@link https://github.com/firebase/firebase-js-sdk/blob/main/packages/firebase/compat/index.d.ts|interface documentation} for method descriptions.
+ */
+class RemoteConfig {
+ get fetchTimeMillis() {
+ return this._storageCache.getLastSuccessfulFetchTimestampMillis() || -1;
+ }
+ get lastFetchStatus() {
+ return this._storageCache.getLastFetchStatus() || 'no-fetch-yet';
+ }
+ constructor(
+ // Required by FirebaseServiceFactory interface.
+ app,
+ // JS doesn't support private yet
+ // (https://github.com/tc39/proposal-class-fields#private-fields), so we hint using an
+ // underscore prefix.
+ /**
+ * @internal
+ */
+ _client,
+ /**
+ * @internal
+ */
+ _storageCache,
+ /**
+ * @internal
+ */
+ _storage,
+ /**
+ * @internal
+ */
+ _logger,
+ /**
+ * @internal
+ */
+ _realtimeHandler) {
+ this.app = app;
+ this._client = _client;
+ this._storageCache = _storageCache;
+ this._storage = _storage;
+ this._logger = _logger;
+ this._realtimeHandler = _realtimeHandler;
+ /**
+ * Tracks completion of initialization promise.
+ * @internal
+ */
+ this._isInitializationComplete = false;
+ this.settings = {
+ fetchTimeoutMillis: DEFAULT_FETCH_TIMEOUT_MILLIS,
+ minimumFetchIntervalMillis: DEFAULT_CACHE_MAX_AGE_MILLIS
+ };
+ this.defaultConfig = {};
+ }
+}
+
+/**
+ * @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.
+ */
+/**
+ * Converts an error event associated with a {@link IDBRequest} to a {@link FirebaseError}.
+ */
+function toFirebaseError(event, errorCode) {
+ const originalError = event.target.error || undefined;
+ return ERROR_FACTORY.create(errorCode, {
+ originalErrorMessage: originalError && originalError?.message
+ });
+}
+/**
+ * A general-purpose store keyed by app + namespace + {@link
+ * ProjectNamespaceKeyFieldValue}.
+ *
+ * <p>The Remote Config SDK can be used with multiple app installations, and each app can interact
+ * with multiple namespaces, so this store uses app (ID + name) and namespace as common parent keys
+ * for a set of key-value pairs. See {@link Storage#createCompositeKey}.
+ *
+ * <p>Visible for testing.
+ */
+const APP_NAMESPACE_STORE = 'app_namespace_store';
+const DB_NAME = 'firebase_remote_config';
+const DB_VERSION = 1;
+// Visible for testing.
+function openDatabase() {
+ return new Promise((resolve, reject) => {
+ try {
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
+ request.onerror = event => {
+ reject(toFirebaseError(event, "storage-open" /* ErrorCode.STORAGE_OPEN */));
+ };
+ request.onsuccess = event => {
+ resolve(event.target.result);
+ };
+ request.onupgradeneeded = event => {
+ const db = event.target.result;
+ // 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 (event.oldVersion) {
+ case 0:
+ db.createObjectStore(APP_NAMESPACE_STORE, {
+ keyPath: 'compositeKey'
+ });
+ }
+ };
+ }
+ catch (error) {
+ reject(ERROR_FACTORY.create("storage-open" /* ErrorCode.STORAGE_OPEN */, {
+ originalErrorMessage: error?.message
+ }));
+ }
+ });
+}
+/**
+ * Abstracts data persistence.
+ */
+class Storage {
+ getLastFetchStatus() {
+ return this.get('last_fetch_status');
+ }
+ setLastFetchStatus(status) {
+ return this.set('last_fetch_status', status);
+ }
+ // This is comparable to a cache entry timestamp. If we need to expire other data, we could
+ // consider adding timestamp to all storage records and an optional max age arg to getters.
+ getLastSuccessfulFetchTimestampMillis() {
+ return this.get('last_successful_fetch_timestamp_millis');
+ }
+ setLastSuccessfulFetchTimestampMillis(timestamp) {
+ return this.set('last_successful_fetch_timestamp_millis', timestamp);
+ }
+ getLastSuccessfulFetchResponse() {
+ return this.get('last_successful_fetch_response');
+ }
+ setLastSuccessfulFetchResponse(response) {
+ return this.set('last_successful_fetch_response', response);
+ }
+ getActiveConfig() {
+ return this.get('active_config');
+ }
+ setActiveConfig(config) {
+ return this.set('active_config', config);
+ }
+ getActiveConfigEtag() {
+ return this.get('active_config_etag');
+ }
+ setActiveConfigEtag(etag) {
+ return this.set('active_config_etag', etag);
+ }
+ getThrottleMetadata() {
+ return this.get('throttle_metadata');
+ }
+ setThrottleMetadata(metadata) {
+ return this.set('throttle_metadata', metadata);
+ }
+ deleteThrottleMetadata() {
+ return this.delete('throttle_metadata');
+ }
+ getCustomSignals() {
+ return this.get('custom_signals');
+ }
+ getRealtimeBackoffMetadata() {
+ return this.get('realtime_backoff_metadata');
+ }
+ setRealtimeBackoffMetadata(realtimeMetadata) {
+ return this.set('realtime_backoff_metadata', realtimeMetadata);
+ }
+ getActiveConfigTemplateVersion() {
+ return this.get('last_known_template_version');
+ }
+ setActiveConfigTemplateVersion(version) {
+ return this.set('last_known_template_version', version);
+ }
+}
+class IndexedDbStorage extends Storage {
+ /**
+ * @param appId enables storage segmentation by app (ID + name).
+ * @param appName enables storage segmentation by app (ID + name).
+ * @param namespace enables storage segmentation by namespace.
+ */
+ constructor(appId, appName, namespace, openDbPromise = openDatabase()) {
+ super();
+ this.appId = appId;
+ this.appName = appName;
+ this.namespace = namespace;
+ this.openDbPromise = openDbPromise;
+ }
+ async setCustomSignals(customSignals) {
+ const db = await this.openDbPromise;
+ const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
+ const storedSignals = await this.getWithTransaction('custom_signals', transaction);
+ const updatedSignals = mergeCustomSignals(customSignals, storedSignals || {});
+ await this.setWithTransaction('custom_signals', updatedSignals, transaction);
+ return updatedSignals;
+ }
+ /**
+ * Gets a value from the database using the provided transaction.
+ *
+ * @param key The key of the value to get.
+ * @param transaction The transaction to use for the operation.
+ * @returns The value associated with the key, or undefined if no such value exists.
+ */
+ async getWithTransaction(key, transaction) {
+ return new Promise((resolve, reject) => {
+ const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
+ const compositeKey = this.createCompositeKey(key);
+ try {
+ const request = objectStore.get(compositeKey);
+ request.onerror = event => {
+ reject(toFirebaseError(event, "storage-get" /* ErrorCode.STORAGE_GET */));
+ };
+ request.onsuccess = event => {
+ const result = event.target.result;
+ if (result) {
+ resolve(result.value);
+ }
+ else {
+ resolve(undefined);
+ }
+ };
+ }
+ catch (e) {
+ reject(ERROR_FACTORY.create("storage-get" /* ErrorCode.STORAGE_GET */, {
+ originalErrorMessage: e?.message
+ }));
+ }
+ });
+ }
+ /**
+ * Sets a value in the database using the provided transaction.
+ *
+ * @param key The key of the value to set.
+ * @param value The value to set.
+ * @param transaction The transaction to use for the operation.
+ * @returns A promise that resolves when the operation is complete.
+ */
+ async setWithTransaction(key, value, transaction) {
+ return new Promise((resolve, reject) => {
+ const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
+ const compositeKey = this.createCompositeKey(key);
+ try {
+ const request = objectStore.put({
+ compositeKey,
+ value
+ });
+ request.onerror = (event) => {
+ reject(toFirebaseError(event, "storage-set" /* ErrorCode.STORAGE_SET */));
+ };
+ request.onsuccess = () => {
+ resolve();
+ };
+ }
+ catch (e) {
+ reject(ERROR_FACTORY.create("storage-set" /* ErrorCode.STORAGE_SET */, {
+ originalErrorMessage: e?.message
+ }));
+ }
+ });
+ }
+ async get(key) {
+ const db = await this.openDbPromise;
+ const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly');
+ return this.getWithTransaction(key, transaction);
+ }
+ async set(key, value) {
+ const db = await this.openDbPromise;
+ const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
+ return this.setWithTransaction(key, value, transaction);
+ }
+ async delete(key) {
+ const db = await this.openDbPromise;
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
+ const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
+ const compositeKey = this.createCompositeKey(key);
+ try {
+ const request = objectStore.delete(compositeKey);
+ request.onerror = (event) => {
+ reject(toFirebaseError(event, "storage-delete" /* ErrorCode.STORAGE_DELETE */));
+ };
+ request.onsuccess = () => {
+ resolve();
+ };
+ }
+ catch (e) {
+ reject(ERROR_FACTORY.create("storage-delete" /* ErrorCode.STORAGE_DELETE */, {
+ originalErrorMessage: e?.message
+ }));
+ }
+ });
+ }
+ // Facilitates composite key functionality (which is unsupported in IE).
+ createCompositeKey(key) {
+ return [this.appId, this.appName, this.namespace, key].join();
+ }
+}
+class InMemoryStorage extends Storage {
+ constructor() {
+ super(...arguments);
+ this.storage = {};
+ }
+ async get(key) {
+ return Promise.resolve(this.storage[key]);
+ }
+ async set(key, value) {
+ this.storage[key] = value;
+ return Promise.resolve(undefined);
+ }
+ async delete(key) {
+ this.storage[key] = undefined;
+ return Promise.resolve();
+ }
+ async setCustomSignals(customSignals) {
+ const storedSignals = (this.storage['custom_signals'] ||
+ {});
+ this.storage['custom_signals'] = mergeCustomSignals(customSignals, storedSignals);
+ return Promise.resolve(this.storage['custom_signals']);
+ }
+}
+function mergeCustomSignals(customSignals, storedSignals) {
+ const combinedSignals = {
+ ...storedSignals,
+ ...customSignals
+ };
+ // Filter out key-value assignments with null values since they are signals being unset
+ const updatedSignals = Object.fromEntries(Object.entries(combinedSignals)
+ .filter(([_, v]) => v !== null)
+ .map(([k, v]) => {
+ // Stringify numbers to store a map of string keys and values which can be sent
+ // as-is in a fetch call.
+ if (typeof v === 'number') {
+ return [k, v.toString()];
+ }
+ return [k, v];
+ }));
+ // Throw an error if the number of custom signals to be stored exceeds the limit
+ if (Object.keys(updatedSignals).length > RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS) {
+ throw ERROR_FACTORY.create("custom-signal-max-allowed-signals" /* ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS */, {
+ maxSignals: RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS
+ });
+ }
+ return updatedSignals;
+}
+
+/**
+ * @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.
+ */
+/**
+ * A memory cache layer over storage to support the SDK's synchronous read requirements.
+ */
+class StorageCache {
+ constructor(storage) {
+ this.storage = storage;
+ }
+ /**
+ * Memory-only getters
+ */
+ getLastFetchStatus() {
+ return this.lastFetchStatus;
+ }
+ getLastSuccessfulFetchTimestampMillis() {
+ return this.lastSuccessfulFetchTimestampMillis;
+ }
+ getActiveConfig() {
+ return this.activeConfig;
+ }
+ getCustomSignals() {
+ return this.customSignals;
+ }
+ /**
+ * Read-ahead getter
+ */
+ async loadFromStorage() {
+ const lastFetchStatusPromise = this.storage.getLastFetchStatus();
+ const lastSuccessfulFetchTimestampMillisPromise = this.storage.getLastSuccessfulFetchTimestampMillis();
+ const activeConfigPromise = this.storage.getActiveConfig();
+ const customSignalsPromise = this.storage.getCustomSignals();
+ // Note:
+ // 1. we consistently check for undefined to avoid clobbering defined values
+ // in memory
+ // 2. we defer awaiting to improve readability, as opposed to destructuring
+ // a Promise.all result, for example
+ const lastFetchStatus = await lastFetchStatusPromise;
+ if (lastFetchStatus) {
+ this.lastFetchStatus = lastFetchStatus;
+ }
+ const lastSuccessfulFetchTimestampMillis = await lastSuccessfulFetchTimestampMillisPromise;
+ if (lastSuccessfulFetchTimestampMillis) {
+ this.lastSuccessfulFetchTimestampMillis =
+ lastSuccessfulFetchTimestampMillis;
+ }
+ const activeConfig = await activeConfigPromise;
+ if (activeConfig) {
+ this.activeConfig = activeConfig;
+ }
+ const customSignals = await customSignalsPromise;
+ if (customSignals) {
+ this.customSignals = customSignals;
+ }
+ }
+ /**
+ * Write-through setters
+ */
+ setLastFetchStatus(status) {
+ this.lastFetchStatus = status;
+ return this.storage.setLastFetchStatus(status);
+ }
+ setLastSuccessfulFetchTimestampMillis(timestampMillis) {
+ this.lastSuccessfulFetchTimestampMillis = timestampMillis;
+ return this.storage.setLastSuccessfulFetchTimestampMillis(timestampMillis);
+ }
+ setActiveConfig(activeConfig) {
+ this.activeConfig = activeConfig;
+ return this.storage.setActiveConfig(activeConfig);
+ }
+ async setCustomSignals(customSignals) {
+ this.customSignals = await this.storage.setCustomSignals(customSignals);
+ }
+}
+
+/**
+ * @license
+ * Copyright 2025 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.
+ */
+// TODO: Consolidate the Visibility monitoring API code into a shared utility function in firebase/util to be used by both packages/database and packages/remote-config.
+/**
+ * Base class to be used if you want to emit events. Call the constructor with
+ * the set of allowed event names.
+ */
+class EventEmitter {
+ constructor(allowedEvents_) {
+ this.allowedEvents_ = allowedEvents_;
+ this.listeners_ = {};
+ util.assert(Array.isArray(allowedEvents_) && allowedEvents_.length > 0, 'Requires a non-empty array');
+ }
+ /**
+ * To be called by derived classes to trigger events.
+ */
+ trigger(eventType, ...varArgs) {
+ if (Array.isArray(this.listeners_[eventType])) {
+ // Clone the list, since callbacks could add/remove listeners.
+ const listeners = [...this.listeners_[eventType]];
+ for (let i = 0; i < listeners.length; i++) {
+ listeners[i].callback.apply(listeners[i].context, varArgs);
+ }
+ }
+ }
+ on(eventType, callback, context) {
+ this.validateEventType_(eventType);
+ this.listeners_[eventType] = this.listeners_[eventType] || [];
+ this.listeners_[eventType].push({ callback, context });
+ const eventData = this.getInitialEvent(eventType);
+ if (eventData) {
+ //@ts-ignore
+ callback.apply(context, eventData);
+ }
+ }
+ off(eventType, callback, context) {
+ this.validateEventType_(eventType);
+ const listeners = this.listeners_[eventType] || [];
+ for (let i = 0; i < listeners.length; i++) {
+ if (listeners[i].callback === callback &&
+ (!context || context === listeners[i].context)) {
+ listeners.splice(i, 1);
+ return;
+ }
+ }
+ }
+ validateEventType_(eventType) {
+ util.assert(this.allowedEvents_.find(et => {
+ return et === eventType;
+ }), 'Unknown event: ' + eventType);
+ }
+}
+
+/**
+ * @license
+ * Copyright 2025 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.
+ */
+// TODO: Consolidate the Visibility monitoring API code into a shared utility function in firebase/util to be used by both packages/database and packages/remote-config.
+class VisibilityMonitor extends EventEmitter {
+ static getInstance() {
+ return new VisibilityMonitor();
+ }
+ constructor() {
+ super(['visible']);
+ let hidden;
+ let visibilityChange;
+ if (typeof document !== 'undefined' &&
+ typeof document.addEventListener !== 'undefined') {
+ if (typeof document['hidden'] !== 'undefined') {
+ // Opera 12.10 and Firefox 18 and later support
+ visibilityChange = 'visibilitychange';
+ hidden = 'hidden';
+ } // @ts-ignore
+ else if (typeof document['mozHidden'] !== 'undefined') {
+ visibilityChange = 'mozvisibilitychange';
+ hidden = 'mozHidden';
+ } // @ts-ignore
+ else if (typeof document['msHidden'] !== 'undefined') {
+ visibilityChange = 'msvisibilitychange';
+ hidden = 'msHidden';
+ } // @ts-ignore
+ else if (typeof document['webkitHidden'] !== 'undefined') {
+ visibilityChange = 'webkitvisibilitychange';
+ hidden = 'webkitHidden';
+ }
+ }
+ // Initially, we always assume we are visible. This ensures that in browsers
+ // without page visibility support or in cases where we are never visible
+ // (e.g. chrome extension), we act as if we are visible, i.e. don't delay
+ // reconnects
+ this.visible_ = true;
+ // @ts-ignore
+ if (visibilityChange) {
+ document.addEventListener(visibilityChange, () => {
+ // @ts-ignore
+ const visible = !document[hidden];
+ if (visible !== this.visible_) {
+ this.visible_ = visible;
+ this.trigger('visible', visible);
+ }
+ }, false);
+ }
+ }
+ getInitialEvent(eventType) {
+ util.assert(eventType === 'visible', 'Unknown event type: ' + eventType);
+ return [this.visible_];
+ }
+}
+
+/**
+ * @license
+ * Copyright 2025 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 API_KEY_HEADER = 'X-Goog-Api-Key';
+const INSTALLATIONS_AUTH_TOKEN_HEADER = 'X-Goog-Firebase-Installations-Auth';
+const ORIGINAL_RETRIES = 8;
+const MAXIMUM_FETCH_ATTEMPTS = 3;
+const NO_BACKOFF_TIME_IN_MILLIS = -1;
+const NO_FAILED_REALTIME_STREAMS = 0;
+const REALTIME_DISABLED_KEY = 'featureDisabled';
+const REALTIME_RETRY_INTERVAL = 'retryIntervalSeconds';
+const TEMPLATE_VERSION_KEY = 'latestTemplateVersionNumber';
+class RealtimeHandler {
+ constructor(firebaseInstallations, storage, sdkVersion, namespace, projectId, apiKey, appId, logger, storageCache, cachingClient) {
+ this.firebaseInstallations = firebaseInstallations;
+ this.storage = storage;
+ this.sdkVersion = sdkVersion;
+ this.namespace = namespace;
+ this.projectId = projectId;
+ this.apiKey = apiKey;
+ this.appId = appId;
+ this.logger = logger;
+ this.storageCache = storageCache;
+ this.cachingClient = cachingClient;
+ this.observers = new Set();
+ this.isConnectionActive = false;
+ this.isRealtimeDisabled = false;
+ this.httpRetriesRemaining = ORIGINAL_RETRIES;
+ this.isInBackground = false;
+ this.decoder = new TextDecoder('utf-8');
+ this.isClosingConnection = false;
+ this.propagateError = (e) => this.observers.forEach(o => o.error?.(e));
+ /**
+ * HTTP status code that the Realtime client should retry on.
+ */
+ this.isStatusCodeRetryable = (statusCode) => {
+ const retryableStatusCodes = [
+ 408, // Request Timeout
+ 429, // Too Many Requests
+ 502, // Bad Gateway
+ 503, // Service Unavailable
+ 504 // Gateway Timeout
+ ];
+ return !statusCode || retryableStatusCodes.includes(statusCode);
+ };
+ void this.setRetriesRemaining();
+ void VisibilityMonitor.getInstance().on('visible', this.onVisibilityChange, this);
+ }
+ async setRetriesRemaining() {
+ // Retrieve number of remaining retries from last session. The minimum retry count being one.
+ const metadata = await this.storage.getRealtimeBackoffMetadata();
+ const numFailedStreams = metadata?.numFailedStreams || 0;
+ this.httpRetriesRemaining = Math.max(ORIGINAL_RETRIES - numFailedStreams, 1);
+ }
+ /**
+ * Increment the number of failed stream attempts, increase the backoff duration, set the backoff
+ * end time to "backoff duration" after `lastFailedStreamTime` and persist the new
+ * values to storage metadata.
+ */
+ async updateBackoffMetadataWithLastFailedStreamConnectionTime(lastFailedStreamTime) {
+ const numFailedStreams = ((await this.storage.getRealtimeBackoffMetadata())?.numFailedStreams ||
+ 0) + 1;
+ const backoffMillis = util.calculateBackoffMillis(numFailedStreams, 60000, 2);
+ await this.storage.setRealtimeBackoffMetadata({
+ backoffEndTimeMillis: new Date(lastFailedStreamTime.getTime() + backoffMillis),
+ numFailedStreams
+ });
+ }
+ /**
+ * Increase the backoff duration with a new end time based on Retry Interval.
+ */
+ async updateBackoffMetadataWithRetryInterval(retryIntervalSeconds) {
+ const currentTime = Date.now();
+ const backoffDurationInMillis = retryIntervalSeconds * 1000;
+ const backoffEndTime = new Date(currentTime + backoffDurationInMillis);
+ const numFailedStreams = 0;
+ await this.storage.setRealtimeBackoffMetadata({
+ backoffEndTimeMillis: backoffEndTime,
+ numFailedStreams
+ });
+ await this.retryHttpConnectionWhenBackoffEnds();
+ }
+ /**
+ * Closes the realtime HTTP connection.
+ * Note: This method is designed to be called only once at a time.
+ * If a call is already in progress, subsequent calls will be ignored.
+ */
+ async closeRealtimeHttpConnection() {
+ if (this.isClosingConnection) {
+ return;
+ }
+ this.isClosingConnection = true;
+ try {
+ if (this.reader) {
+ await this.reader.cancel();
+ }
+ }
+ catch (e) {
+ // The network connection was lost, so cancel() failed.
+ // This is expected in a disconnected state, so we can safely ignore the error.
+ this.logger.debug('Failed to cancel the reader, connection was lost.');
+ }
+ finally {
+ this.reader = undefined;
+ }
+ if (this.controller) {
+ await this.controller.abort();
+ this.controller = undefined;
+ }
+ this.isClosingConnection = false;
+ }
+ async resetRealtimeBackoff() {
+ await this.storage.setRealtimeBackoffMetadata({
+ backoffEndTimeMillis: new Date(-1),
+ numFailedStreams: 0
+ });
+ }
+ resetRetryCount() {
+ this.httpRetriesRemaining = ORIGINAL_RETRIES;
+ }
+ /**
+ * Assembles the request headers and body and executes the fetch request to
+ * establish the real-time streaming connection. This is the "worker" method
+ * that performs the actual network communication.
+ */
+ async establishRealtimeConnection(url, installationId, installationTokenResult, signal) {
+ const eTagValue = await this.storage.getActiveConfigEtag();
+ const lastKnownVersionNumber = await this.storage.getActiveConfigTemplateVersion();
+ const headers = {
+ [API_KEY_HEADER]: this.apiKey,
+ [INSTALLATIONS_AUTH_TOKEN_HEADER]: installationTokenResult,
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'If-None-Match': eTagValue || '*',
+ 'Content-Encoding': 'gzip'
+ };
+ const requestBody = {
+ project: this.projectId,
+ namespace: this.namespace,
+ lastKnownVersionNumber,
+ appId: this.appId,
+ sdkVersion: this.sdkVersion,
+ appInstanceId: installationId
+ };
+ const response = await fetch(url, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(requestBody),
+ signal
+ });
+ return response;
+ }
+ getRealtimeUrl() {
+ const urlBase = window.FIREBASE_REMOTE_CONFIG_URL_BASE ||
+ 'https://firebaseremoteconfigrealtime.googleapis.com';
+ const urlString = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:streamFetchInvalidations?key=${this.apiKey}`;
+ return new URL(urlString);
+ }
+ async createRealtimeConnection() {
+ const [installationId, installationTokenResult] = await Promise.all([
+ this.firebaseInstallations.getId(),
+ this.firebaseInstallations.getToken(false)
+ ]);
+ this.controller = new AbortController();
+ const url = this.getRealtimeUrl();
+ const realtimeConnection = await this.establishRealtimeConnection(url, installationId, installationTokenResult, this.controller.signal);
+ return realtimeConnection;
+ }
+ /**
+ * Retries HTTP stream connection asyncly in random time intervals.
+ */
+ async retryHttpConnectionWhenBackoffEnds() {
+ let backoffMetadata = await this.storage.getRealtimeBackoffMetadata();
+ if (!backoffMetadata) {
+ backoffMetadata = {
+ backoffEndTimeMillis: new Date(NO_BACKOFF_TIME_IN_MILLIS),
+ numFailedStreams: NO_FAILED_REALTIME_STREAMS
+ };
+ }
+ const backoffEndTime = new Date(backoffMetadata.backoffEndTimeMillis).getTime();
+ const currentTime = Date.now();
+ const retryMillis = Math.max(0, backoffEndTime - currentTime);
+ await this.makeRealtimeHttpConnection(retryMillis);
+ }
+ setIsHttpConnectionRunning(connectionRunning) {
+ this.isConnectionActive = connectionRunning;
+ }
+ /**
+ * Combines the check and set operations to prevent multiple asynchronous
+ * calls from redundantly starting an HTTP connection. This ensures that
+ * only one attempt is made at a time.
+ */
+ checkAndSetHttpConnectionFlagIfNotRunning() {
+ const canMakeConnection = this.canEstablishStreamConnection();
+ if (canMakeConnection) {
+ this.setIsHttpConnectionRunning(true);
+ }
+ return canMakeConnection;
+ }
+ fetchResponseIsUpToDate(fetchResponse, lastKnownVersion) {
+ // If there is a config, make sure its version is >= the last known version.
+ if (fetchResponse.config != null && fetchResponse.templateVersion) {
+ return fetchResponse.templateVersion >= lastKnownVersion;
+ }
+ // If there isn't a config, return true if the fetch was successful and backend had no update.
+ // Else, it returned an out of date config.
+ return this.storageCache.getLastFetchStatus() === 'success';
+ }
+ parseAndValidateConfigUpdateMessage(message) {
+ const left = message.indexOf('{');
+ const right = message.indexOf('}', left);
+ if (left < 0 || right < 0) {
+ return '';
+ }
+ return left >= right ? '' : message.substring(left, right + 1);
+ }
+ isEventListenersEmpty() {
+ return this.observers.size === 0;
+ }
+ getRandomInt(max) {
+ return Math.floor(Math.random() * max);
+ }
+ executeAllListenerCallbacks(configUpdate) {
+ this.observers.forEach(observer => observer.next(configUpdate));
+ }
+ /**
+ * Compares two configuration objects and returns a set of keys that have changed.
+ * A key is considered changed if it's new, removed, or has a different value.
+ */
+ getChangedParams(newConfig, oldConfig) {
+ const changedKeys = new Set();
+ const newKeys = new Set(Object.keys(newConfig || {}));
+ const oldKeys = new Set(Object.keys(oldConfig || {}));
+ for (const key of newKeys) {
+ if (!oldKeys.has(key) || newConfig[key] !== oldConfig[key]) {
+ changedKeys.add(key);
+ }
+ }
+ for (const key of oldKeys) {
+ if (!newKeys.has(key)) {
+ changedKeys.add(key);
+ }
+ }
+ return changedKeys;
+ }
+ async fetchLatestConfig(remainingAttempts, targetVersion) {
+ const remainingAttemptsAfterFetch = remainingAttempts - 1;
+ const currentAttempt = MAXIMUM_FETCH_ATTEMPTS - remainingAttemptsAfterFetch;
+ const customSignals = this.storageCache.getCustomSignals();
+ if (customSignals) {
+ this.logger.debug(`Fetching config with custom signals: ${JSON.stringify(customSignals)}`);
+ }
+ const abortSignal = new RemoteConfigAbortSignal();
+ try {
+ const fetchRequest = {
+ cacheMaxAgeMillis: 0,
+ signal: abortSignal,
+ customSignals,
+ fetchType: 'REALTIME',
+ fetchAttempt: currentAttempt
+ };
+ const fetchResponse = await this.cachingClient.fetch(fetchRequest);
+ let activatedConfigs = await this.storage.getActiveConfig();
+ if (!this.fetchResponseIsUpToDate(fetchResponse, targetVersion)) {
+ this.logger.debug("Fetched template version is the same as SDK's current version." +
+ ' Retrying fetch.');
+ // Continue fetching until template version number is greater than current.
+ await this.autoFetch(remainingAttemptsAfterFetch, targetVersion);
+ return;
+ }
+ if (fetchResponse.config == null) {
+ this.logger.debug('The fetch succeeded, but the backend had no updates.');
+ return;
+ }
+ if (activatedConfigs == null) {
+ activatedConfigs = {};
+ }
+ const updatedKeys = this.getChangedParams(fetchResponse.config, activatedConfigs);
+ if (updatedKeys.size === 0) {
+ this.logger.debug('Config was fetched, but no params changed.');
+ return;
+ }
+ const configUpdate = {
+ getUpdatedKeys() {
+ return new Set(updatedKeys);
+ }
+ };
+ this.executeAllListenerCallbacks(configUpdate);
+ }
+ catch (e) {
+ const errorMessage = e instanceof Error ? e.message : String(e);
+ const error = ERROR_FACTORY.create("update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */, {
+ originalErrorMessage: `Failed to auto-fetch config update: ${errorMessage}`
+ });
+ this.propagateError(error);
+ }
+ }
+ async autoFetch(remainingAttempts, targetVersion) {
+ if (remainingAttempts === 0) {
+ const error = ERROR_FACTORY.create("update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */, {
+ originalErrorMessage: 'Unable to fetch the latest version of the template.'
+ });
+ this.propagateError(error);
+ return;
+ }
+ const timeTillFetchSeconds = this.getRandomInt(4);
+ const timeTillFetchInMiliseconds = timeTillFetchSeconds * 1000;
+ await new Promise(resolve => setTimeout(resolve, timeTillFetchInMiliseconds));
+ await this.fetchLatestConfig(remainingAttempts, targetVersion);
+ }
+ /**
+ * Processes a stream of real-time messages for configuration updates.
+ * This method reassembles fragmented messages, validates and parses the JSON,
+ * and automatically fetches a new config if a newer template version is available.
+ * It also handles server-specified retry intervals and propagates errors for
+ * invalid messages or when real-time updates are disabled.
+ */
+ async handleNotifications(reader) {
+ let partialConfigUpdateMessage;
+ let currentConfigUpdateMessage = '';
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ break;
+ }
+ partialConfigUpdateMessage = this.decoder.decode(value, { stream: true });
+ currentConfigUpdateMessage += partialConfigUpdateMessage;
+ if (partialConfigUpdateMessage.includes('}')) {
+ currentConfigUpdateMessage = this.parseAndValidateConfigUpdateMessage(currentConfigUpdateMessage);
+ if (currentConfigUpdateMessage.length === 0) {
+ continue;
+ }
+ try {
+ const jsonObject = JSON.parse(currentConfigUpdateMessage);
+ if (this.isEventListenersEmpty()) {
+ break;
+ }
+ if (REALTIME_DISABLED_KEY in jsonObject &&
+ jsonObject[REALTIME_DISABLED_KEY] === true) {
+ const error = ERROR_FACTORY.create("realtime-unavailable" /* ErrorCode.CONFIG_UPDATE_UNAVAILABLE */, {
+ originalErrorMessage: 'The server is temporarily unavailable. Try again in a few minutes.'
+ });
+ this.propagateError(error);
+ break;
+ }
+ if (TEMPLATE_VERSION_KEY in jsonObject) {
+ const oldTemplateVersion = await this.storage.getActiveConfigTemplateVersion();
+ const targetTemplateVersion = Number(jsonObject[TEMPLATE_VERSION_KEY]);
+ if (oldTemplateVersion &&
+ targetTemplateVersion > oldTemplateVersion) {
+ await this.autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion);
+ }
+ }
+ // This field in the response indicates that the realtime request should retry after the
+ // specified interval to establish a long-lived connection. This interval extends the
+ // backoff duration without affecting the number of retries, so it will not enter an
+ // exponential backoff state.
+ if (REALTIME_RETRY_INTERVAL in jsonObject) {
+ const retryIntervalSeconds = Number(jsonObject[REALTIME_RETRY_INTERVAL]);
+ await this.updateBackoffMetadataWithRetryInterval(retryIntervalSeconds);
+ }
+ }
+ catch (e) {
+ this.logger.debug('Unable to parse latest config update message.', e);
+ const errorMessage = e instanceof Error ? e.message : String(e);
+ this.propagateError(ERROR_FACTORY.create("update-message-invalid" /* ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID */, {
+ originalErrorMessage: errorMessage
+ }));
+ }
+ currentConfigUpdateMessage = '';
+ }
+ }
+ }
+ async listenForNotifications(reader) {
+ try {
+ await this.handleNotifications(reader);
+ }
+ catch (e) {
+ // If the real-time connection is at an unexpected lifecycle state when the app is
+ // backgrounded, it's expected closing the connection will throw an exception.
+ if (!this.isInBackground) {
+ // Otherwise, the real-time server connection was closed due to a transient issue.
+ this.logger.debug('Real-time connection was closed due to an exception.');
+ }
+ }
+ }
+ /**
+ * Open the real-time connection, begin listening for updates, and auto-fetch when an update is
+ * received.
+ *
+ * If the connection is successful, this method will block on its thread while it reads the
+ * chunk-encoded HTTP body. When the connection closes, it attempts to reestablish the stream.
+ */
+ async prepareAndBeginRealtimeHttpStream() {
+ if (!this.checkAndSetHttpConnectionFlagIfNotRunning()) {
+ return;
+ }
+ let backoffMetadata = await this.storage.getRealtimeBackoffMetadata();
+ if (!backoffMetadata) {
+ backoffMetadata = {
+ backoffEndTimeMillis: new Date(NO_BACKOFF_TIME_IN_MILLIS),
+ numFailedStreams: NO_FAILED_REALTIME_STREAMS
+ };
+ }
+ const backoffEndTime = backoffMetadata.backoffEndTimeMillis.getTime();
+ if (Date.now() < backoffEndTime) {
+ await this.retryHttpConnectionWhenBackoffEnds();
+ return;
+ }
+ let response;
+ let responseCode;
+ try {
+ response = await this.createRealtimeConnection();
+ responseCode = response.status;
+ if (response.ok && response.body) {
+ this.resetRetryCount();
+ await this.resetRealtimeBackoff();
+ const reader = response.body.getReader();
+ this.reader = reader;
+ // Start listening for realtime notifications.
+ await this.listenForNotifications(reader);
+ }
+ }
+ catch (error) {
+ if (this.isInBackground) {
+ // It's possible the app was backgrounded while the connection was open, which
+ // threw an exception trying to read the response. No real error here, so treat
+ // this as a success, even if we haven't read a 200 response code yet.
+ this.resetRetryCount();
+ }
+ else {
+ //there might have been a transient error so the client will retry the connection.
+ this.logger.debug('Exception connecting to real-time RC backend. Retrying the connection...:', error);
+ }
+ }
+ finally {
+ // Close HTTP connection and associated streams.
+ await this.closeRealtimeHttpConnection();
+ this.setIsHttpConnectionRunning(false);
+ // Update backoff metadata if the connection failed in the foreground.
+ const connectionFailed = !this.isInBackground &&
+ (responseCode === undefined ||
+ this.isStatusCodeRetryable(responseCode));
+ if (connectionFailed) {
+ await this.updateBackoffMetadataWithLastFailedStreamConnectionTime(new Date());
+ }
+ // If responseCode is null then no connection was made to server and the SDK should still retry.
+ if (connectionFailed || response?.ok) {
+ await this.retryHttpConnectionWhenBackoffEnds();
+ }
+ else {
+ const errorMessage = `Unable to connect to the server. HTTP status code: ${responseCode}`;
+ const firebaseError = ERROR_FACTORY.create("stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */, {
+ originalErrorMessage: errorMessage
+ });
+ this.propagateError(firebaseError);
+ }
+ }
+ }
+ /**
+ * Checks whether connection can be made or not based on some conditions
+ * @returns booelean
+ */
+ canEstablishStreamConnection() {
+ const hasActiveListeners = this.observers.size > 0;
+ const isNotDisabled = !this.isRealtimeDisabled;
+ const isNoConnectionActive = !this.isConnectionActive;
+ const inForeground = !this.isInBackground;
+ return (hasActiveListeners &&
+ isNotDisabled &&
+ isNoConnectionActive &&
+ inForeground);
+ }
+ async makeRealtimeHttpConnection(delayMillis) {
+ if (!this.canEstablishStreamConnection()) {
+ return;
+ }
+ if (this.httpRetriesRemaining > 0) {
+ this.httpRetriesRemaining--;
+ await new Promise(resolve => setTimeout(resolve, delayMillis));
+ void this.prepareAndBeginRealtimeHttpStream();
+ }
+ else if (!this.isInBackground) {
+ const error = ERROR_FACTORY.create("stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */, {
+ originalErrorMessage: 'Unable to connect to the server. Check your connection and try again.'
+ });
+ this.propagateError(error);
+ }
+ }
+ async beginRealtime() {
+ if (this.observers.size > 0) {
+ await this.makeRealtimeHttpConnection(0);
+ }
+ }
+ /**
+ * Adds an observer to the realtime updates.
+ * @param observer The observer to add.
+ */
+ addObserver(observer) {
+ this.observers.add(observer);
+ void this.beginRealtime();
+ }
+ /**
+ * Removes an observer from the realtime updates.
+ * @param observer The observer to remove.
+ */
+ removeObserver(observer) {
+ if (this.observers.has(observer)) {
+ this.observers.delete(observer);
+ }
+ }
+ /**
+ * Handles changes to the application's visibility state, managing the real-time connection.
+ *
+ * When the application is moved to the background, this method closes the existing
+ * real-time connection to save resources. When the application returns to the
+ * foreground, it attempts to re-establish the connection.
+ */
+ async onVisibilityChange(visible) {
+ this.isInBackground = !visible;
+ if (!visible) {
+ await this.closeRealtimeHttpConnection();
+ }
+ else if (visible) {
+ await this.beginRealtime();
+ }
+ }
+}
+
+/**
+ * @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 registerRemoteConfig() {
+ app._registerComponent(new component.Component(RC_COMPONENT_NAME, remoteConfigFactory, "PUBLIC" /* ComponentType.PUBLIC */).setMultipleInstances(true));
+ app.registerVersion(name, version);
+ // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation
+ app.registerVersion(name, version, 'cjs2020');
+ function remoteConfigFactory(container, { options }) {
+ /* Dependencies */
+ // getImmediate for FirebaseApp will always succeed
+ const app$1 = container.getProvider('app').getImmediate();
+ // The following call will always succeed because rc has `import '@firebase/installations'`
+ const installations = container
+ .getProvider('installations-internal')
+ .getImmediate();
+ // Normalizes optional inputs.
+ const { projectId, apiKey, appId } = app$1.options;
+ if (!projectId) {
+ throw ERROR_FACTORY.create("registration-project-id" /* ErrorCode.REGISTRATION_PROJECT_ID */);
+ }
+ if (!apiKey) {
+ throw ERROR_FACTORY.create("registration-api-key" /* ErrorCode.REGISTRATION_API_KEY */);
+ }
+ if (!appId) {
+ throw ERROR_FACTORY.create("registration-app-id" /* ErrorCode.REGISTRATION_APP_ID */);
+ }
+ const namespace = options?.templateId || 'firebase';
+ const storage = util.isIndexedDBAvailable()
+ ? new IndexedDbStorage(appId, app$1.name, namespace)
+ : new InMemoryStorage();
+ const storageCache = new StorageCache(storage);
+ const logger$1 = new logger.Logger(name);
+ // Sets ERROR as the default log level.
+ // See RemoteConfig#setLogLevel for corresponding normalization to ERROR log level.
+ logger$1.logLevel = logger.LogLevel.ERROR;
+ const restClient = new RestClient(installations,
+ // Uses the JS SDK version, by which the RC package version can be deduced, if necessary.
+ app.SDK_VERSION, namespace, projectId, apiKey, appId);
+ const retryingClient = new RetryingClient(restClient, storage);
+ const cachingClient = new CachingClient(retryingClient, storage, storageCache, logger$1);
+ const realtimeHandler = new RealtimeHandler(installations, storage, app.SDK_VERSION, namespace, projectId, apiKey, appId, logger$1, storageCache, cachingClient);
+ const remoteConfigInstance = new RemoteConfig(app$1, cachingClient, storageCache, storage, logger$1, realtimeHandler);
+ // Starts warming cache.
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ ensureInitialized(remoteConfigInstance);
+ return remoteConfigInstance;
+ }
+}
+
+/**
+ * @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.
+ */
+// This API is put in a separate file, so we can stub fetchConfig and activate in tests.
+// It's not possible to stub standalone functions from the same module.
+/**
+ *
+ * Performs fetch and activate operations, as a convenience.
+ *
+ * @param remoteConfig - The {@link RemoteConfig} instance.
+ *
+ * @returns A `Promise` which resolves to true if the current call activated the fetched configs.
+ * If the fetched configs were already activated, the `Promise` will resolve to false.
+ *
+ * @public
+ */
+async function fetchAndActivate(remoteConfig) {
+ remoteConfig = util.getModularInstance(remoteConfig);
+ await fetchConfig(remoteConfig);
+ return activate(remoteConfig);
+}
+/**
+ * This method provides two different checks:
+ *
+ * 1. Check if IndexedDB exists in the browser environment.
+ * 2. Check if the current browser context allows IndexedDB `open()` calls.
+ *
+ * @returns A `Promise` which resolves to true if a {@link RemoteConfig} instance
+ * can be initialized in this environment, or false if it cannot.
+ * @public
+ */
+async function isSupported() {
+ if (!util.isIndexedDBAvailable()) {
+ return false;
+ }
+ try {
+ const isDBOpenable = await util.validateIndexedDBOpenable();
+ return isDBOpenable;
+ }
+ catch (error) {
+ return false;
+ }
+}
+
+/**
+ * The Firebase Remote Config Web SDK.
+ * This SDK does not work in a Node.js environment.
+ *
+ * @packageDocumentation
+ */
+/** register component and version */
+registerRemoteConfig();
+
+exports.activate = activate;
+exports.ensureInitialized = ensureInitialized;
+exports.fetchAndActivate = fetchAndActivate;
+exports.fetchConfig = fetchConfig;
+exports.getAll = getAll;
+exports.getBoolean = getBoolean;
+exports.getNumber = getNumber;
+exports.getRemoteConfig = getRemoteConfig;
+exports.getString = getString;
+exports.getValue = getValue;
+exports.isSupported = isSupported;
+exports.onConfigUpdate = onConfigUpdate;
+exports.setCustomSignals = setCustomSignals;
+exports.setLogLevel = setLogLevel;
+//# sourceMappingURL=index.cjs.js.map