@depup/firebase__remote-config 0.8.1-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +31 -0
  2. package/changes.json +10 -0
  3. package/dist/esm/index.esm.js +2199 -0
  4. package/dist/esm/index.esm.js.map +1 -0
  5. package/dist/esm/package.json +1 -0
  6. package/dist/esm/src/abt/experiment.d.ts +13 -0
  7. package/dist/esm/src/api.d.ts +144 -0
  8. package/dist/esm/src/api2.d.ts +40 -0
  9. package/dist/esm/src/client/caching_client.d.ts +46 -0
  10. package/dist/esm/src/client/eventEmitter.d.ts +39 -0
  11. package/dist/esm/src/client/realtime_handler.d.ts +141 -0
  12. package/dist/esm/src/client/remote_config_fetch_client.d.ts +104 -0
  13. package/dist/esm/src/client/rest_client.d.ts +41 -0
  14. package/dist/esm/src/client/retrying_client.d.ts +50 -0
  15. package/dist/esm/src/client/visibility_monitor.d.ts +23 -0
  16. package/dist/esm/src/constants.d.ts +20 -0
  17. package/dist/esm/src/errors.d.ts +87 -0
  18. package/dist/esm/src/index.d.ts +14 -0
  19. package/dist/esm/src/language.d.ts +26 -0
  20. package/dist/esm/src/public_types.d.ts +274 -0
  21. package/dist/esm/src/register.d.ts +2 -0
  22. package/dist/esm/src/remote_config.d.ts +98 -0
  23. package/dist/esm/src/storage/storage.d.ts +118 -0
  24. package/dist/esm/src/storage/storage_cache.d.ts +51 -0
  25. package/dist/esm/src/value.d.ts +26 -0
  26. package/dist/esm/test/setup.d.ts +17 -0
  27. package/dist/index.cjs.js +2216 -0
  28. package/dist/index.cjs.js.map +1 -0
  29. package/dist/remote-config-public.d.ts +441 -0
  30. package/dist/remote-config.d.ts +441 -0
  31. package/dist/src/abt/experiment.d.ts +13 -0
  32. package/dist/src/api.d.ts +144 -0
  33. package/dist/src/api2.d.ts +40 -0
  34. package/dist/src/client/caching_client.d.ts +46 -0
  35. package/dist/src/client/eventEmitter.d.ts +39 -0
  36. package/dist/src/client/realtime_handler.d.ts +141 -0
  37. package/dist/src/client/remote_config_fetch_client.d.ts +104 -0
  38. package/dist/src/client/rest_client.d.ts +41 -0
  39. package/dist/src/client/retrying_client.d.ts +50 -0
  40. package/dist/src/client/visibility_monitor.d.ts +23 -0
  41. package/dist/src/constants.d.ts +20 -0
  42. package/dist/src/errors.d.ts +87 -0
  43. package/dist/src/global_index.d.ts +674 -0
  44. package/dist/src/index.d.ts +14 -0
  45. package/dist/src/language.d.ts +26 -0
  46. package/dist/src/public_types.d.ts +274 -0
  47. package/dist/src/register.d.ts +2 -0
  48. package/dist/src/remote_config.d.ts +98 -0
  49. package/dist/src/storage/storage.d.ts +118 -0
  50. package/dist/src/storage/storage_cache.d.ts +51 -0
  51. package/dist/src/tsdoc-metadata.json +11 -0
  52. package/dist/src/value.d.ts +26 -0
  53. package/dist/test/setup.d.ts +17 -0
  54. package/package.json +93 -0
@@ -0,0 +1,2199 @@
1
+ import { _getProvider, getApp, _registerComponent, registerVersion, SDK_VERSION } from '@firebase/app';
2
+ import { ErrorFactory, FirebaseError, getModularInstance, deepEqual, calculateBackoffMillis, assert, isIndexedDBAvailable, validateIndexedDBOpenable } from '@firebase/util';
3
+ import { Component } from '@firebase/component';
4
+ import { LogLevel, Logger } from '@firebase/logger';
5
+ import '@firebase/installations';
6
+
7
+ const name = "@firebase/remote-config";
8
+ const version = "0.8.1";
9
+
10
+ /**
11
+ * @license
12
+ * Copyright 2019 Google LLC
13
+ *
14
+ * Licensed under the Apache License, Version 2.0 (the "License");
15
+ * you may not use this file except in compliance with the License.
16
+ * You may obtain a copy of the License at
17
+ *
18
+ * http://www.apache.org/licenses/LICENSE-2.0
19
+ *
20
+ * Unless required by applicable law or agreed to in writing, software
21
+ * distributed under the License is distributed on an "AS IS" BASIS,
22
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23
+ * See the License for the specific language governing permissions and
24
+ * limitations under the License.
25
+ */
26
+ /**
27
+ * Shims a minimal AbortSignal.
28
+ *
29
+ * <p>AbortController's AbortSignal conveniently decouples fetch timeout logic from other aspects
30
+ * of networking, such as retries. Firebase doesn't use AbortController enough to justify a
31
+ * polyfill recommendation, like we do with the Fetch API, but this minimal shim can easily be
32
+ * swapped out if/when we do.
33
+ */
34
+ class RemoteConfigAbortSignal {
35
+ constructor() {
36
+ this.listeners = [];
37
+ }
38
+ addEventListener(listener) {
39
+ this.listeners.push(listener);
40
+ }
41
+ abort() {
42
+ this.listeners.forEach(listener => listener());
43
+ }
44
+ }
45
+
46
+ /**
47
+ * @license
48
+ * Copyright 2020 Google LLC
49
+ *
50
+ * Licensed under the Apache License, Version 2.0 (the "License");
51
+ * you may not use this file except in compliance with the License.
52
+ * You may obtain a copy of the License at
53
+ *
54
+ * http://www.apache.org/licenses/LICENSE-2.0
55
+ *
56
+ * Unless required by applicable law or agreed to in writing, software
57
+ * distributed under the License is distributed on an "AS IS" BASIS,
58
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
59
+ * See the License for the specific language governing permissions and
60
+ * limitations under the License.
61
+ */
62
+ const RC_COMPONENT_NAME = 'remote-config';
63
+ const RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 100;
64
+ const RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH = 250;
65
+ const RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH = 500;
66
+
67
+ /**
68
+ * @license
69
+ * Copyright 2019 Google LLC
70
+ *
71
+ * Licensed under the Apache License, Version 2.0 (the "License");
72
+ * you may not use this file except in compliance with the License.
73
+ * You may obtain a copy of the License at
74
+ *
75
+ * http://www.apache.org/licenses/LICENSE-2.0
76
+ *
77
+ * Unless required by applicable law or agreed to in writing, software
78
+ * distributed under the License is distributed on an "AS IS" BASIS,
79
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
80
+ * See the License for the specific language governing permissions and
81
+ * limitations under the License.
82
+ */
83
+ const ERROR_DESCRIPTION_MAP = {
84
+ ["already-initialized" /* ErrorCode.ALREADY_INITIALIZED */]: 'Remote Config already initialized',
85
+ ["registration-window" /* ErrorCode.REGISTRATION_WINDOW */]: 'Undefined window object. This SDK only supports usage in a browser environment.',
86
+ ["registration-project-id" /* ErrorCode.REGISTRATION_PROJECT_ID */]: 'Undefined project identifier. Check Firebase app initialization.',
87
+ ["registration-api-key" /* ErrorCode.REGISTRATION_API_KEY */]: 'Undefined API key. Check Firebase app initialization.',
88
+ ["registration-app-id" /* ErrorCode.REGISTRATION_APP_ID */]: 'Undefined app identifier. Check Firebase app initialization.',
89
+ ["storage-open" /* ErrorCode.STORAGE_OPEN */]: 'Error thrown when opening storage. Original error: {$originalErrorMessage}.',
90
+ ["storage-get" /* ErrorCode.STORAGE_GET */]: 'Error thrown when reading from storage. Original error: {$originalErrorMessage}.',
91
+ ["storage-set" /* ErrorCode.STORAGE_SET */]: 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.',
92
+ ["storage-delete" /* ErrorCode.STORAGE_DELETE */]: 'Error thrown when deleting from storage. Original error: {$originalErrorMessage}.',
93
+ ["fetch-client-network" /* ErrorCode.FETCH_NETWORK */]: 'Fetch client failed to connect to a network. Check Internet connection.' +
94
+ ' Original error: {$originalErrorMessage}.',
95
+ ["fetch-timeout" /* ErrorCode.FETCH_TIMEOUT */]: 'The config fetch request timed out. ' +
96
+ ' Configure timeout using "fetchTimeoutMillis" SDK setting.',
97
+ ["fetch-throttle" /* ErrorCode.FETCH_THROTTLE */]: 'The config fetch request timed out while in an exponential backoff state.' +
98
+ ' Configure timeout using "fetchTimeoutMillis" SDK setting.' +
99
+ ' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.',
100
+ ["fetch-client-parse" /* ErrorCode.FETCH_PARSE */]: 'Fetch client could not parse response.' +
101
+ ' Original error: {$originalErrorMessage}.',
102
+ ["fetch-status" /* ErrorCode.FETCH_STATUS */]: 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.',
103
+ ["indexed-db-unavailable" /* ErrorCode.INDEXED_DB_UNAVAILABLE */]: 'Indexed DB is not supported by current browser',
104
+ ["custom-signal-max-allowed-signals" /* ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS */]: 'Setting more than {$maxSignals} custom signals is not supported.',
105
+ ["stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */]: 'The stream was not able to connect to the backend: {$originalErrorMessage}.',
106
+ ["realtime-unavailable" /* ErrorCode.CONFIG_UPDATE_UNAVAILABLE */]: 'The Realtime service is unavailable: {$originalErrorMessage}',
107
+ ["update-message-invalid" /* ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID */]: 'The stream invalidation message was unparsable: {$originalErrorMessage}',
108
+ ["update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */]: 'Unable to fetch the latest config: {$originalErrorMessage}',
109
+ ["analytics-unavailable" /* ErrorCode.ANALYTICS_UNAVAILABLE */]: 'Connection to Firebase Analytics failed: {$originalErrorMessage}'
110
+ };
111
+ const ERROR_FACTORY = new ErrorFactory('remoteconfig' /* service */, 'Remote Config' /* service name */, ERROR_DESCRIPTION_MAP);
112
+ // Note how this is like typeof/instanceof, but for ErrorCode.
113
+ function hasErrorCode(e, errorCode) {
114
+ return e instanceof FirebaseError && e.code.indexOf(errorCode) !== -1;
115
+ }
116
+
117
+ /**
118
+ * @license
119
+ * Copyright 2019 Google LLC
120
+ *
121
+ * Licensed under the Apache License, Version 2.0 (the "License");
122
+ * you may not use this file except in compliance with the License.
123
+ * You may obtain a copy of the License at
124
+ *
125
+ * http://www.apache.org/licenses/LICENSE-2.0
126
+ *
127
+ * Unless required by applicable law or agreed to in writing, software
128
+ * distributed under the License is distributed on an "AS IS" BASIS,
129
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
130
+ * See the License for the specific language governing permissions and
131
+ * limitations under the License.
132
+ */
133
+ const DEFAULT_VALUE_FOR_BOOLEAN = false;
134
+ const DEFAULT_VALUE_FOR_STRING = '';
135
+ const DEFAULT_VALUE_FOR_NUMBER = 0;
136
+ const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
137
+ class Value {
138
+ constructor(_source, _value = DEFAULT_VALUE_FOR_STRING) {
139
+ this._source = _source;
140
+ this._value = _value;
141
+ }
142
+ asString() {
143
+ return this._value;
144
+ }
145
+ asBoolean() {
146
+ if (this._source === 'static') {
147
+ return DEFAULT_VALUE_FOR_BOOLEAN;
148
+ }
149
+ return BOOLEAN_TRUTHY_VALUES.indexOf(this._value.toLowerCase()) >= 0;
150
+ }
151
+ asNumber() {
152
+ if (this._source === 'static') {
153
+ return DEFAULT_VALUE_FOR_NUMBER;
154
+ }
155
+ let num = Number(this._value);
156
+ if (isNaN(num)) {
157
+ num = DEFAULT_VALUE_FOR_NUMBER;
158
+ }
159
+ return num;
160
+ }
161
+ getSource() {
162
+ return this._source;
163
+ }
164
+ }
165
+
166
+ class Experiment {
167
+ constructor(rc) {
168
+ this.storage = rc._storage;
169
+ this.logger = rc._logger;
170
+ this.analyticsProvider = rc._analyticsProvider;
171
+ }
172
+ async updateActiveExperiments(latestExperiments) {
173
+ const currentActiveExperiments = (await this.storage.getActiveExperiments()) || new Set();
174
+ const experimentInfoMap = this.createExperimentInfoMap(latestExperiments);
175
+ this.addActiveExperiments(experimentInfoMap);
176
+ this.removeInactiveExperiments(currentActiveExperiments, experimentInfoMap);
177
+ return this.storage.setActiveExperiments(new Set(experimentInfoMap.keys()));
178
+ }
179
+ createExperimentInfoMap(latestExperiments) {
180
+ const experimentInfoMap = new Map();
181
+ for (const experiment of latestExperiments) {
182
+ experimentInfoMap.set(experiment.experimentId, experiment);
183
+ }
184
+ return experimentInfoMap;
185
+ }
186
+ addActiveExperiments(experimentInfoMap) {
187
+ const customProperty = {};
188
+ for (const [experimentId, experimentInfo] of experimentInfoMap.entries()) {
189
+ customProperty[`firebase${experimentId}`] = experimentInfo.variantId;
190
+ }
191
+ this.addExperimentToAnalytics(customProperty);
192
+ }
193
+ removeInactiveExperiments(currentActiveExperiments, experimentInfoMap) {
194
+ const customProperty = {};
195
+ for (const experimentId of currentActiveExperiments) {
196
+ if (!experimentInfoMap.has(experimentId)) {
197
+ customProperty[`firebase${experimentId}`] = null;
198
+ }
199
+ }
200
+ this.addExperimentToAnalytics(customProperty);
201
+ }
202
+ addExperimentToAnalytics(customProperty) {
203
+ if (Object.keys(customProperty).length === 0) {
204
+ return;
205
+ }
206
+ try {
207
+ const analytics = this.analyticsProvider.getImmediate({ optional: true });
208
+ if (analytics) {
209
+ analytics.setUserProperties(customProperty);
210
+ analytics.logEvent(`set_firebase_experiment_state`);
211
+ }
212
+ else {
213
+ this.logger.warn(`Analytics import failed. Verify if you have imported Firebase Analytics in your app code.`);
214
+ }
215
+ }
216
+ catch (error) {
217
+ throw ERROR_FACTORY.create("analytics-unavailable" /* ErrorCode.ANALYTICS_UNAVAILABLE */, {
218
+ originalErrorMessage: error?.message
219
+ });
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * @license
226
+ * Copyright 2020 Google LLC
227
+ *
228
+ * Licensed under the Apache License, Version 2.0 (the "License");
229
+ * you may not use this file except in compliance with the License.
230
+ * You may obtain a copy of the License at
231
+ *
232
+ * http://www.apache.org/licenses/LICENSE-2.0
233
+ *
234
+ * Unless required by applicable law or agreed to in writing, software
235
+ * distributed under the License is distributed on an "AS IS" BASIS,
236
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
237
+ * See the License for the specific language governing permissions and
238
+ * limitations under the License.
239
+ */
240
+ /**
241
+ *
242
+ * @param app - The {@link @firebase/app#FirebaseApp} instance.
243
+ * @param options - Optional. The {@link RemoteConfigOptions} with which to instantiate the
244
+ * Remote Config instance.
245
+ * @returns A {@link RemoteConfig} instance.
246
+ *
247
+ * @public
248
+ */
249
+ function getRemoteConfig(app = getApp(), options = {}) {
250
+ app = getModularInstance(app);
251
+ const rcProvider = _getProvider(app, RC_COMPONENT_NAME);
252
+ if (rcProvider.isInitialized()) {
253
+ const initialOptions = rcProvider.getOptions();
254
+ if (deepEqual(initialOptions, options)) {
255
+ return rcProvider.getImmediate();
256
+ }
257
+ throw ERROR_FACTORY.create("already-initialized" /* ErrorCode.ALREADY_INITIALIZED */);
258
+ }
259
+ rcProvider.initialize({ options });
260
+ const rc = rcProvider.getImmediate();
261
+ if (options.initialFetchResponse) {
262
+ // We use these initial writes as the initialization promise since they will hydrate the same
263
+ // fields that `storageCache.loadFromStorage` would set.
264
+ rc._initializePromise = Promise.all([
265
+ rc._storage.setLastSuccessfulFetchResponse(options.initialFetchResponse),
266
+ rc._storage.setActiveConfigEtag(options.initialFetchResponse?.eTag || ''),
267
+ rc._storage.setActiveConfigTemplateVersion(options.initialFetchResponse.templateVersion || 0),
268
+ rc._storageCache.setLastSuccessfulFetchTimestampMillis(Date.now()),
269
+ rc._storageCache.setLastFetchStatus('success'),
270
+ rc._storageCache.setActiveConfig(options.initialFetchResponse?.config || {})
271
+ ]).then();
272
+ // The `storageCache` methods above set their in-memory fields synchronously, so it's
273
+ // safe to declare our initialization complete at this point.
274
+ rc._isInitializationComplete = true;
275
+ }
276
+ return rc;
277
+ }
278
+ /**
279
+ * Makes the last fetched config available to the getters.
280
+ * @param remoteConfig - The {@link RemoteConfig} instance.
281
+ * @returns A `Promise` which resolves to true if the current call activated the fetched configs.
282
+ * If the fetched configs were already activated, the `Promise` will resolve to false.
283
+ *
284
+ * @public
285
+ */
286
+ async function activate(remoteConfig) {
287
+ const rc = getModularInstance(remoteConfig);
288
+ const [lastSuccessfulFetchResponse, activeConfigEtag] = await Promise.all([
289
+ rc._storage.getLastSuccessfulFetchResponse(),
290
+ rc._storage.getActiveConfigEtag()
291
+ ]);
292
+ if (!lastSuccessfulFetchResponse ||
293
+ !lastSuccessfulFetchResponse.config ||
294
+ !lastSuccessfulFetchResponse.eTag ||
295
+ !lastSuccessfulFetchResponse.templateVersion ||
296
+ lastSuccessfulFetchResponse.eTag === activeConfigEtag) {
297
+ // Either there is no successful fetched config, or is the same as current active
298
+ // config.
299
+ return false;
300
+ }
301
+ const experiment = new Experiment(rc);
302
+ const updateActiveExperiments = lastSuccessfulFetchResponse.experiments
303
+ ? experiment.updateActiveExperiments(lastSuccessfulFetchResponse.experiments)
304
+ : Promise.resolve();
305
+ await Promise.all([
306
+ rc._storageCache.setActiveConfig(lastSuccessfulFetchResponse.config),
307
+ rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag),
308
+ rc._storage.setActiveConfigTemplateVersion(lastSuccessfulFetchResponse.templateVersion),
309
+ updateActiveExperiments
310
+ ]);
311
+ return true;
312
+ }
313
+ /**
314
+ * Ensures the last activated config are available to the getters.
315
+ * @param remoteConfig - The {@link RemoteConfig} instance.
316
+ *
317
+ * @returns A `Promise` that resolves when the last activated config is available to the getters.
318
+ * @public
319
+ */
320
+ function ensureInitialized(remoteConfig) {
321
+ const rc = getModularInstance(remoteConfig);
322
+ if (!rc._initializePromise) {
323
+ rc._initializePromise = rc._storageCache.loadFromStorage().then(() => {
324
+ rc._isInitializationComplete = true;
325
+ });
326
+ }
327
+ return rc._initializePromise;
328
+ }
329
+ /**
330
+ * Fetches and caches configuration from the Remote Config service.
331
+ * @param remoteConfig - The {@link RemoteConfig} instance.
332
+ * @public
333
+ */
334
+ async function fetchConfig(remoteConfig) {
335
+ const rc = getModularInstance(remoteConfig);
336
+ // Aborts the request after the given timeout, causing the fetch call to
337
+ // reject with an `AbortError`.
338
+ //
339
+ // <p>Aborting after the request completes is a no-op, so we don't need a
340
+ // corresponding `clearTimeout`.
341
+ //
342
+ // Locating abort logic here because:
343
+ // * it uses a developer setting (timeout)
344
+ // * it applies to all retries (like curl's max-time arg)
345
+ // * it is consistent with the Fetch API's signal input
346
+ const abortSignal = new RemoteConfigAbortSignal();
347
+ setTimeout(async () => {
348
+ // Note a very low delay, eg < 10ms, can elapse before listeners are initialized.
349
+ abortSignal.abort();
350
+ }, rc.settings.fetchTimeoutMillis);
351
+ const customSignals = rc._storageCache.getCustomSignals();
352
+ if (customSignals) {
353
+ rc._logger.debug(`Fetching config with custom signals: ${JSON.stringify(customSignals)}`);
354
+ }
355
+ // Catches *all* errors thrown by client so status can be set consistently.
356
+ try {
357
+ await rc._client.fetch({
358
+ cacheMaxAgeMillis: rc.settings.minimumFetchIntervalMillis,
359
+ signal: abortSignal,
360
+ customSignals
361
+ });
362
+ await rc._storageCache.setLastFetchStatus('success');
363
+ }
364
+ catch (e) {
365
+ const lastFetchStatus = hasErrorCode(e, "fetch-throttle" /* ErrorCode.FETCH_THROTTLE */)
366
+ ? 'throttle'
367
+ : 'failure';
368
+ await rc._storageCache.setLastFetchStatus(lastFetchStatus);
369
+ throw e;
370
+ }
371
+ }
372
+ /**
373
+ * Gets all config.
374
+ *
375
+ * @param remoteConfig - The {@link RemoteConfig} instance.
376
+ * @returns All config.
377
+ *
378
+ * @public
379
+ */
380
+ function getAll(remoteConfig) {
381
+ const rc = getModularInstance(remoteConfig);
382
+ return getAllKeys(rc._storageCache.getActiveConfig(), rc.defaultConfig).reduce((allConfigs, key) => {
383
+ allConfigs[key] = getValue(remoteConfig, key);
384
+ return allConfigs;
385
+ }, {});
386
+ }
387
+ /**
388
+ * Gets the value for the given key as a boolean.
389
+ *
390
+ * Convenience method for calling <code>remoteConfig.getValue(key).asBoolean()</code>.
391
+ *
392
+ * @param remoteConfig - The {@link RemoteConfig} instance.
393
+ * @param key - The name of the parameter.
394
+ *
395
+ * @returns The value for the given key as a boolean.
396
+ * @public
397
+ */
398
+ function getBoolean(remoteConfig, key) {
399
+ return getValue(getModularInstance(remoteConfig), key).asBoolean();
400
+ }
401
+ /**
402
+ * Gets the value for the given key as a number.
403
+ *
404
+ * Convenience method for calling <code>remoteConfig.getValue(key).asNumber()</code>.
405
+ *
406
+ * @param remoteConfig - The {@link RemoteConfig} instance.
407
+ * @param key - The name of the parameter.
408
+ *
409
+ * @returns The value for the given key as a number.
410
+ *
411
+ * @public
412
+ */
413
+ function getNumber(remoteConfig, key) {
414
+ return getValue(getModularInstance(remoteConfig), key).asNumber();
415
+ }
416
+ /**
417
+ * Gets the value for the given key as a string.
418
+ * Convenience method for calling <code>remoteConfig.getValue(key).asString()</code>.
419
+ *
420
+ * @param remoteConfig - The {@link RemoteConfig} instance.
421
+ * @param key - The name of the parameter.
422
+ *
423
+ * @returns The value for the given key as a string.
424
+ *
425
+ * @public
426
+ */
427
+ function getString(remoteConfig, key) {
428
+ return getValue(getModularInstance(remoteConfig), key).asString();
429
+ }
430
+ /**
431
+ * Gets the {@link Value} for the given key.
432
+ *
433
+ * @param remoteConfig - The {@link RemoteConfig} instance.
434
+ * @param key - The name of the parameter.
435
+ *
436
+ * @returns The value for the given key.
437
+ *
438
+ * @public
439
+ */
440
+ function getValue(remoteConfig, key) {
441
+ const rc = getModularInstance(remoteConfig);
442
+ if (!rc._isInitializationComplete) {
443
+ rc._logger.debug(`A value was requested for key "${key}" before SDK initialization completed.` +
444
+ ' Await on ensureInitialized if the intent was to get a previously activated value.');
445
+ }
446
+ const activeConfig = rc._storageCache.getActiveConfig();
447
+ if (activeConfig && activeConfig[key] !== undefined) {
448
+ return new Value('remote', activeConfig[key]);
449
+ }
450
+ else if (rc.defaultConfig && rc.defaultConfig[key] !== undefined) {
451
+ return new Value('default', String(rc.defaultConfig[key]));
452
+ }
453
+ rc._logger.debug(`Returning static value for key "${key}".` +
454
+ ' Define a default or remote value if this is unintentional.');
455
+ return new Value('static');
456
+ }
457
+ /**
458
+ * Defines the log level to use.
459
+ *
460
+ * @param remoteConfig - The {@link RemoteConfig} instance.
461
+ * @param logLevel - The log level to set.
462
+ *
463
+ * @public
464
+ */
465
+ function setLogLevel(remoteConfig, logLevel) {
466
+ const rc = getModularInstance(remoteConfig);
467
+ switch (logLevel) {
468
+ case 'debug':
469
+ rc._logger.logLevel = LogLevel.DEBUG;
470
+ break;
471
+ case 'silent':
472
+ rc._logger.logLevel = LogLevel.SILENT;
473
+ break;
474
+ default:
475
+ rc._logger.logLevel = LogLevel.ERROR;
476
+ }
477
+ }
478
+ /**
479
+ * Dedupes and returns an array of all the keys of the received objects.
480
+ */
481
+ function getAllKeys(obj1 = {}, obj2 = {}) {
482
+ return Object.keys({ ...obj1, ...obj2 });
483
+ }
484
+ /**
485
+ * Sets the custom signals for the app instance.
486
+ *
487
+ * @param remoteConfig - The {@link RemoteConfig} instance.
488
+ * @param customSignals - Map (key, value) of the custom signals to be set for the app instance. If
489
+ * a key already exists, the value is overwritten. Setting the value of a custom signal to null
490
+ * unsets the signal. The signals will be persisted locally on the client.
491
+ *
492
+ * @public
493
+ */
494
+ async function setCustomSignals(remoteConfig, customSignals) {
495
+ const rc = getModularInstance(remoteConfig);
496
+ if (Object.keys(customSignals).length === 0) {
497
+ return;
498
+ }
499
+ // eslint-disable-next-line guard-for-in
500
+ for (const key in customSignals) {
501
+ if (key.length > RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH) {
502
+ rc._logger.error(`Custom signal key ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH}.`);
503
+ return;
504
+ }
505
+ const value = customSignals[key];
506
+ if (typeof value === 'string' &&
507
+ value.length > RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH) {
508
+ rc._logger.error(`Value supplied for custom signal ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH}.`);
509
+ return;
510
+ }
511
+ }
512
+ try {
513
+ await rc._storageCache.setCustomSignals(customSignals);
514
+ }
515
+ catch (error) {
516
+ rc._logger.error(`Error encountered while setting custom signals: ${error}`);
517
+ }
518
+ }
519
+ // TODO: Add public document for the Remote Config Realtime API guide on the Web Platform.
520
+ /**
521
+ * Starts listening for real-time config updates from the Remote Config backend and automatically
522
+ * fetches updates from the Remote Config backend when they are available.
523
+ *
524
+ * @remarks
525
+ * If a connection to the Remote Config backend is not already open, calling this method will
526
+ * open it. Multiple listeners can be added by calling this method again, but subsequent calls
527
+ * re-use the same connection to the backend.
528
+ *
529
+ * @param remoteConfig - The {@link RemoteConfig} instance.
530
+ * @param observer - The {@link ConfigUpdateObserver} to be notified of config updates.
531
+ * @returns An {@link Unsubscribe} function to remove the listener.
532
+ *
533
+ * @public
534
+ */
535
+ function onConfigUpdate(remoteConfig, observer) {
536
+ const rc = getModularInstance(remoteConfig);
537
+ rc._realtimeHandler.addObserver(observer);
538
+ return () => {
539
+ rc._realtimeHandler.removeObserver(observer);
540
+ };
541
+ }
542
+
543
+ /**
544
+ * @license
545
+ * Copyright 2019 Google LLC
546
+ *
547
+ * Licensed under the Apache License, Version 2.0 (the "License");
548
+ * you may not use this file except in compliance with the License.
549
+ * You may obtain a copy of the License at
550
+ *
551
+ * http://www.apache.org/licenses/LICENSE-2.0
552
+ *
553
+ * Unless required by applicable law or agreed to in writing, software
554
+ * distributed under the License is distributed on an "AS IS" BASIS,
555
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
556
+ * See the License for the specific language governing permissions and
557
+ * limitations under the License.
558
+ */
559
+ /**
560
+ * Implements the {@link RemoteConfigClient} abstraction with success response caching.
561
+ *
562
+ * <p>Comparable to the browser's Cache API for responses, but the Cache API requires a Service
563
+ * Worker, which requires HTTPS, which would significantly complicate SDK installation. Also, the
564
+ * Cache API doesn't support matching entries by time.
565
+ */
566
+ class CachingClient {
567
+ constructor(client, storage, storageCache, logger) {
568
+ this.client = client;
569
+ this.storage = storage;
570
+ this.storageCache = storageCache;
571
+ this.logger = logger;
572
+ }
573
+ /**
574
+ * Returns true if the age of the cached fetched configs is less than or equal to
575
+ * {@link Settings#minimumFetchIntervalInSeconds}.
576
+ *
577
+ * <p>This is comparable to passing `headers = { 'Cache-Control': max-age <maxAge> }` to the
578
+ * native Fetch API.
579
+ *
580
+ * <p>Visible for testing.
581
+ */
582
+ isCachedDataFresh(cacheMaxAgeMillis, lastSuccessfulFetchTimestampMillis) {
583
+ // Cache can only be fresh if it's populated.
584
+ if (!lastSuccessfulFetchTimestampMillis) {
585
+ this.logger.debug('Config fetch cache check. Cache unpopulated.');
586
+ return false;
587
+ }
588
+ // Calculates age of cache entry.
589
+ const cacheAgeMillis = Date.now() - lastSuccessfulFetchTimestampMillis;
590
+ const isCachedDataFresh = cacheAgeMillis <= cacheMaxAgeMillis;
591
+ this.logger.debug('Config fetch cache check.' +
592
+ ` Cache age millis: ${cacheAgeMillis}.` +
593
+ ` Cache max age millis (minimumFetchIntervalMillis setting): ${cacheMaxAgeMillis}.` +
594
+ ` Is cache hit: ${isCachedDataFresh}.`);
595
+ return isCachedDataFresh;
596
+ }
597
+ async fetch(request) {
598
+ // Reads from persisted storage to avoid cache miss if callers don't wait on initialization.
599
+ const [lastSuccessfulFetchTimestampMillis, lastSuccessfulFetchResponse] = await Promise.all([
600
+ this.storage.getLastSuccessfulFetchTimestampMillis(),
601
+ this.storage.getLastSuccessfulFetchResponse()
602
+ ]);
603
+ // Exits early on cache hit.
604
+ if (lastSuccessfulFetchResponse &&
605
+ this.isCachedDataFresh(request.cacheMaxAgeMillis, lastSuccessfulFetchTimestampMillis)) {
606
+ return lastSuccessfulFetchResponse;
607
+ }
608
+ // Deviates from pure decorator by not honoring a passed ETag since we don't have a public API
609
+ // that allows the caller to pass an ETag.
610
+ request.eTag =
611
+ lastSuccessfulFetchResponse && lastSuccessfulFetchResponse.eTag;
612
+ // Falls back to service on cache miss.
613
+ const response = await this.client.fetch(request);
614
+ // Fetch throws for non-success responses, so success is guaranteed here.
615
+ const storageOperations = [
616
+ // Uses write-through cache for consistency with synchronous public API.
617
+ this.storageCache.setLastSuccessfulFetchTimestampMillis(Date.now())
618
+ ];
619
+ if (response.status === 200) {
620
+ // Caches response only if it has changed, ie non-304 responses.
621
+ storageOperations.push(this.storage.setLastSuccessfulFetchResponse(response));
622
+ }
623
+ await Promise.all(storageOperations);
624
+ return response;
625
+ }
626
+ }
627
+
628
+ /**
629
+ * @license
630
+ * Copyright 2019 Google LLC
631
+ *
632
+ * Licensed under the Apache License, Version 2.0 (the "License");
633
+ * you may not use this file except in compliance with the License.
634
+ * You may obtain a copy of the License at
635
+ *
636
+ * http://www.apache.org/licenses/LICENSE-2.0
637
+ *
638
+ * Unless required by applicable law or agreed to in writing, software
639
+ * distributed under the License is distributed on an "AS IS" BASIS,
640
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
641
+ * See the License for the specific language governing permissions and
642
+ * limitations under the License.
643
+ */
644
+ /**
645
+ * Attempts to get the most accurate browser language setting.
646
+ *
647
+ * <p>Adapted from getUserLanguage in packages/auth/src/utils.js for TypeScript.
648
+ *
649
+ * <p>Defers default language specification to server logic for consistency.
650
+ *
651
+ * @param navigatorLanguage Enables tests to override read-only {@link NavigatorLanguage}.
652
+ */
653
+ function getUserLanguage(navigatorLanguage = navigator) {
654
+ return (
655
+ // Most reliable, but only supported in Chrome/Firefox.
656
+ (navigatorLanguage.languages && navigatorLanguage.languages[0]) ||
657
+ // Supported in most browsers, but returns the language of the browser
658
+ // UI, not the language set in browser settings.
659
+ navigatorLanguage.language
660
+ // Polyfill otherwise.
661
+ );
662
+ }
663
+
664
+ /**
665
+ * @license
666
+ * Copyright 2019 Google LLC
667
+ *
668
+ * Licensed under the Apache License, Version 2.0 (the "License");
669
+ * you may not use this file except in compliance with the License.
670
+ * You may obtain a copy of the License at
671
+ *
672
+ * http://www.apache.org/licenses/LICENSE-2.0
673
+ *
674
+ * Unless required by applicable law or agreed to in writing, software
675
+ * distributed under the License is distributed on an "AS IS" BASIS,
676
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
677
+ * See the License for the specific language governing permissions and
678
+ * limitations under the License.
679
+ */
680
+ /**
681
+ * Implements the Client abstraction for the Remote Config REST API.
682
+ */
683
+ class RestClient {
684
+ constructor(firebaseInstallations, sdkVersion, namespace, projectId, apiKey, appId) {
685
+ this.firebaseInstallations = firebaseInstallations;
686
+ this.sdkVersion = sdkVersion;
687
+ this.namespace = namespace;
688
+ this.projectId = projectId;
689
+ this.apiKey = apiKey;
690
+ this.appId = appId;
691
+ }
692
+ /**
693
+ * Fetches from the Remote Config REST API.
694
+ *
695
+ * @throws a {@link ErrorCode.FETCH_NETWORK} error if {@link GlobalFetch#fetch} can't
696
+ * connect to the network.
697
+ * @throws a {@link ErrorCode.FETCH_PARSE} error if {@link Response#json} can't parse the
698
+ * fetch response.
699
+ * @throws a {@link ErrorCode.FETCH_STATUS} error if the service returns an HTTP error status.
700
+ */
701
+ async fetch(request) {
702
+ const [installationId, installationToken] = await Promise.all([
703
+ this.firebaseInstallations.getId(),
704
+ this.firebaseInstallations.getToken()
705
+ ]);
706
+ const urlBase = window.FIREBASE_REMOTE_CONFIG_URL_BASE ||
707
+ 'https://firebaseremoteconfig.googleapis.com';
708
+ const url = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:fetch?key=${this.apiKey}`;
709
+ const headers = {
710
+ 'Content-Type': 'application/json',
711
+ 'Content-Encoding': 'gzip',
712
+ // Deviates from pure decorator by not passing max-age header since we don't currently have
713
+ // service behavior using that header.
714
+ 'If-None-Match': request.eTag || '*'
715
+ // TODO: Add this header once CORS error is fixed internally.
716
+ //'X-Firebase-RC-Fetch-Type': `${fetchType}/${fetchAttempt}`
717
+ };
718
+ const requestBody = {
719
+ /* eslint-disable camelcase */
720
+ sdk_version: this.sdkVersion,
721
+ app_instance_id: installationId,
722
+ app_instance_id_token: installationToken,
723
+ app_id: this.appId,
724
+ language_code: getUserLanguage(),
725
+ custom_signals: request.customSignals
726
+ /* eslint-enable camelcase */
727
+ };
728
+ const options = {
729
+ method: 'POST',
730
+ headers,
731
+ body: JSON.stringify(requestBody)
732
+ };
733
+ // This logic isn't REST-specific, but shimming abort logic isn't worth another decorator.
734
+ const fetchPromise = fetch(url, options);
735
+ const timeoutPromise = new Promise((_resolve, reject) => {
736
+ // Maps async event listener to Promise API.
737
+ request.signal.addEventListener(() => {
738
+ // Emulates https://heycam.github.io/webidl/#aborterror
739
+ const error = new Error('The operation was aborted.');
740
+ error.name = 'AbortError';
741
+ reject(error);
742
+ });
743
+ });
744
+ let response;
745
+ try {
746
+ await Promise.race([fetchPromise, timeoutPromise]);
747
+ response = await fetchPromise;
748
+ }
749
+ catch (originalError) {
750
+ let errorCode = "fetch-client-network" /* ErrorCode.FETCH_NETWORK */;
751
+ if (originalError?.name === 'AbortError') {
752
+ errorCode = "fetch-timeout" /* ErrorCode.FETCH_TIMEOUT */;
753
+ }
754
+ throw ERROR_FACTORY.create(errorCode, {
755
+ originalErrorMessage: originalError?.message
756
+ });
757
+ }
758
+ let status = response.status;
759
+ // Normalizes nullable header to optional.
760
+ const responseEtag = response.headers.get('ETag') || undefined;
761
+ let config;
762
+ let state;
763
+ let templateVersion;
764
+ let experiments;
765
+ // JSON parsing throws SyntaxError if the response body isn't a JSON string.
766
+ // Requesting application/json and checking for a 200 ensures there's JSON data.
767
+ if (response.status === 200) {
768
+ let responseBody;
769
+ try {
770
+ responseBody = await response.json();
771
+ }
772
+ catch (originalError) {
773
+ throw ERROR_FACTORY.create("fetch-client-parse" /* ErrorCode.FETCH_PARSE */, {
774
+ originalErrorMessage: originalError?.message
775
+ });
776
+ }
777
+ config = responseBody['entries'];
778
+ state = responseBody['state'];
779
+ templateVersion = responseBody['templateVersion'];
780
+ experiments = responseBody['experimentDescriptions'];
781
+ }
782
+ // Normalizes based on legacy state.
783
+ if (state === 'INSTANCE_STATE_UNSPECIFIED') {
784
+ status = 500;
785
+ }
786
+ else if (state === 'NO_CHANGE') {
787
+ status = 304;
788
+ }
789
+ else if (state === 'NO_TEMPLATE' || state === 'EMPTY_CONFIG') {
790
+ // These cases can be fixed remotely, so normalize to safe value.
791
+ config = {};
792
+ experiments = [];
793
+ }
794
+ // Normalize to exception-based control flow for non-success cases.
795
+ // Encapsulates HTTP specifics in this class as much as possible. Status is still the best for
796
+ // differentiating success states (200 from 304; the state body param is undefined in a
797
+ // standard 304).
798
+ if (status !== 304 && status !== 200) {
799
+ throw ERROR_FACTORY.create("fetch-status" /* ErrorCode.FETCH_STATUS */, {
800
+ httpStatus: status
801
+ });
802
+ }
803
+ return { status, eTag: responseEtag, config, templateVersion, experiments };
804
+ }
805
+ }
806
+
807
+ /**
808
+ * @license
809
+ * Copyright 2019 Google LLC
810
+ *
811
+ * Licensed under the Apache License, Version 2.0 (the "License");
812
+ * you may not use this file except in compliance with the License.
813
+ * You may obtain a copy of the License at
814
+ *
815
+ * http://www.apache.org/licenses/LICENSE-2.0
816
+ *
817
+ * Unless required by applicable law or agreed to in writing, software
818
+ * distributed under the License is distributed on an "AS IS" BASIS,
819
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
820
+ * See the License for the specific language governing permissions and
821
+ * limitations under the License.
822
+ */
823
+ /**
824
+ * Supports waiting on a backoff by:
825
+ *
826
+ * <ul>
827
+ * <li>Promisifying setTimeout, so we can set a timeout in our Promise chain</li>
828
+ * <li>Listening on a signal bus for abort events, just like the Fetch API</li>
829
+ * <li>Failing in the same way the Fetch API fails, so timing out a live request and a throttled
830
+ * request appear the same.</li>
831
+ * </ul>
832
+ *
833
+ * <p>Visible for testing.
834
+ */
835
+ function setAbortableTimeout(signal, throttleEndTimeMillis) {
836
+ return new Promise((resolve, reject) => {
837
+ // Derives backoff from given end time, normalizing negative numbers to zero.
838
+ const backoffMillis = Math.max(throttleEndTimeMillis - Date.now(), 0);
839
+ const timeout = setTimeout(resolve, backoffMillis);
840
+ // Adds listener, rather than sets onabort, because signal is a shared object.
841
+ signal.addEventListener(() => {
842
+ clearTimeout(timeout);
843
+ // If the request completes before this timeout, the rejection has no effect.
844
+ reject(ERROR_FACTORY.create("fetch-throttle" /* ErrorCode.FETCH_THROTTLE */, {
845
+ throttleEndTimeMillis
846
+ }));
847
+ });
848
+ });
849
+ }
850
+ /**
851
+ * Returns true if the {@link Error} indicates a fetch request may succeed later.
852
+ */
853
+ function isRetriableError(e) {
854
+ if (!(e instanceof FirebaseError) || !e.customData) {
855
+ return false;
856
+ }
857
+ // Uses string index defined by ErrorData, which FirebaseError implements.
858
+ const httpStatus = Number(e.customData['httpStatus']);
859
+ return (httpStatus === 429 ||
860
+ httpStatus === 500 ||
861
+ httpStatus === 503 ||
862
+ httpStatus === 504);
863
+ }
864
+ /**
865
+ * Decorates a Client with retry logic.
866
+ *
867
+ * <p>Comparable to CachingClient, but uses backoff logic instead of cache max age and doesn't cache
868
+ * responses (because the SDK has no use for error responses).
869
+ */
870
+ class RetryingClient {
871
+ constructor(client, storage) {
872
+ this.client = client;
873
+ this.storage = storage;
874
+ }
875
+ async fetch(request) {
876
+ const throttleMetadata = (await this.storage.getThrottleMetadata()) || {
877
+ backoffCount: 0,
878
+ throttleEndTimeMillis: Date.now()
879
+ };
880
+ return this.attemptFetch(request, throttleMetadata);
881
+ }
882
+ /**
883
+ * A recursive helper for attempting a fetch request repeatedly.
884
+ *
885
+ * @throws any non-retriable errors.
886
+ */
887
+ async attemptFetch(request, { throttleEndTimeMillis, backoffCount }) {
888
+ // Starts with a (potentially zero) timeout to support resumption from stored state.
889
+ // Ensures the throttle end time is honored if the last attempt timed out.
890
+ // Note the SDK will never make a request if the fetch timeout expires at this point.
891
+ await setAbortableTimeout(request.signal, throttleEndTimeMillis);
892
+ try {
893
+ const response = await this.client.fetch(request);
894
+ // Note the SDK only clears throttle state if response is success or non-retriable.
895
+ await this.storage.deleteThrottleMetadata();
896
+ return response;
897
+ }
898
+ catch (e) {
899
+ if (!isRetriableError(e)) {
900
+ throw e;
901
+ }
902
+ // Increments backoff state.
903
+ const throttleMetadata = {
904
+ throttleEndTimeMillis: Date.now() + calculateBackoffMillis(backoffCount),
905
+ backoffCount: backoffCount + 1
906
+ };
907
+ // Persists state.
908
+ await this.storage.setThrottleMetadata(throttleMetadata);
909
+ return this.attemptFetch(request, throttleMetadata);
910
+ }
911
+ }
912
+ }
913
+
914
+ /**
915
+ * @license
916
+ * Copyright 2019 Google LLC
917
+ *
918
+ * Licensed under the Apache License, Version 2.0 (the "License");
919
+ * you may not use this file except in compliance with the License.
920
+ * You may obtain a copy of the License at
921
+ *
922
+ * http://www.apache.org/licenses/LICENSE-2.0
923
+ *
924
+ * Unless required by applicable law or agreed to in writing, software
925
+ * distributed under the License is distributed on an "AS IS" BASIS,
926
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
927
+ * See the License for the specific language governing permissions and
928
+ * limitations under the License.
929
+ */
930
+ const DEFAULT_FETCH_TIMEOUT_MILLIS = 60 * 1000; // One minute
931
+ const DEFAULT_CACHE_MAX_AGE_MILLIS = 12 * 60 * 60 * 1000; // Twelve hours.
932
+ /**
933
+ * Encapsulates business logic mapping network and storage dependencies to the public SDK API.
934
+ *
935
+ * See {@link https://github.com/firebase/firebase-js-sdk/blob/main/packages/firebase/compat/index.d.ts|interface documentation} for method descriptions.
936
+ */
937
+ class RemoteConfig {
938
+ get fetchTimeMillis() {
939
+ return this._storageCache.getLastSuccessfulFetchTimestampMillis() || -1;
940
+ }
941
+ get lastFetchStatus() {
942
+ return this._storageCache.getLastFetchStatus() || 'no-fetch-yet';
943
+ }
944
+ constructor(
945
+ // Required by FirebaseServiceFactory interface.
946
+ app,
947
+ // JS doesn't support private yet
948
+ // (https://github.com/tc39/proposal-class-fields#private-fields), so we hint using an
949
+ // underscore prefix.
950
+ /**
951
+ * @internal
952
+ */
953
+ _client,
954
+ /**
955
+ * @internal
956
+ */
957
+ _storageCache,
958
+ /**
959
+ * @internal
960
+ */
961
+ _storage,
962
+ /**
963
+ * @internal
964
+ */
965
+ _logger,
966
+ /**
967
+ * @internal
968
+ */
969
+ _realtimeHandler,
970
+ /**
971
+ * @internal
972
+ */
973
+ _analyticsProvider) {
974
+ this.app = app;
975
+ this._client = _client;
976
+ this._storageCache = _storageCache;
977
+ this._storage = _storage;
978
+ this._logger = _logger;
979
+ this._realtimeHandler = _realtimeHandler;
980
+ this._analyticsProvider = _analyticsProvider;
981
+ /**
982
+ * Tracks completion of initialization promise.
983
+ * @internal
984
+ */
985
+ this._isInitializationComplete = false;
986
+ this.settings = {
987
+ fetchTimeoutMillis: DEFAULT_FETCH_TIMEOUT_MILLIS,
988
+ minimumFetchIntervalMillis: DEFAULT_CACHE_MAX_AGE_MILLIS
989
+ };
990
+ this.defaultConfig = {};
991
+ }
992
+ }
993
+
994
+ /**
995
+ * @license
996
+ * Copyright 2019 Google LLC
997
+ *
998
+ * Licensed under the Apache License, Version 2.0 (the "License");
999
+ * you may not use this file except in compliance with the License.
1000
+ * You may obtain a copy of the License at
1001
+ *
1002
+ * http://www.apache.org/licenses/LICENSE-2.0
1003
+ *
1004
+ * Unless required by applicable law or agreed to in writing, software
1005
+ * distributed under the License is distributed on an "AS IS" BASIS,
1006
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1007
+ * See the License for the specific language governing permissions and
1008
+ * limitations under the License.
1009
+ */
1010
+ /**
1011
+ * Converts an error event associated with a {@link IDBRequest} to a {@link FirebaseError}.
1012
+ */
1013
+ function toFirebaseError(event, errorCode) {
1014
+ const originalError = event.target.error || undefined;
1015
+ return ERROR_FACTORY.create(errorCode, {
1016
+ originalErrorMessage: originalError && originalError?.message
1017
+ });
1018
+ }
1019
+ /**
1020
+ * A general-purpose store keyed by app + namespace + {@link
1021
+ * ProjectNamespaceKeyFieldValue}.
1022
+ *
1023
+ * <p>The Remote Config SDK can be used with multiple app installations, and each app can interact
1024
+ * with multiple namespaces, so this store uses app (ID + name) and namespace as common parent keys
1025
+ * for a set of key-value pairs. See {@link Storage#createCompositeKey}.
1026
+ *
1027
+ * <p>Visible for testing.
1028
+ */
1029
+ const APP_NAMESPACE_STORE = 'app_namespace_store';
1030
+ const DB_NAME = 'firebase_remote_config';
1031
+ const DB_VERSION = 1;
1032
+ // Visible for testing.
1033
+ function openDatabase() {
1034
+ return new Promise((resolve, reject) => {
1035
+ try {
1036
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
1037
+ request.onerror = event => {
1038
+ reject(toFirebaseError(event, "storage-open" /* ErrorCode.STORAGE_OPEN */));
1039
+ };
1040
+ request.onsuccess = event => {
1041
+ resolve(event.target.result);
1042
+ };
1043
+ request.onupgradeneeded = event => {
1044
+ const db = event.target.result;
1045
+ // We don't use 'break' in this switch statement, the fall-through
1046
+ // behavior is what we want, because if there are multiple versions between
1047
+ // the old version and the current version, we want ALL the migrations
1048
+ // that correspond to those versions to run, not only the last one.
1049
+ // eslint-disable-next-line default-case
1050
+ switch (event.oldVersion) {
1051
+ case 0:
1052
+ db.createObjectStore(APP_NAMESPACE_STORE, {
1053
+ keyPath: 'compositeKey'
1054
+ });
1055
+ }
1056
+ };
1057
+ }
1058
+ catch (error) {
1059
+ reject(ERROR_FACTORY.create("storage-open" /* ErrorCode.STORAGE_OPEN */, {
1060
+ originalErrorMessage: error?.message
1061
+ }));
1062
+ }
1063
+ });
1064
+ }
1065
+ /**
1066
+ * Abstracts data persistence.
1067
+ */
1068
+ class Storage {
1069
+ getLastFetchStatus() {
1070
+ return this.get('last_fetch_status');
1071
+ }
1072
+ setLastFetchStatus(status) {
1073
+ return this.set('last_fetch_status', status);
1074
+ }
1075
+ // This is comparable to a cache entry timestamp. If we need to expire other data, we could
1076
+ // consider adding timestamp to all storage records and an optional max age arg to getters.
1077
+ getLastSuccessfulFetchTimestampMillis() {
1078
+ return this.get('last_successful_fetch_timestamp_millis');
1079
+ }
1080
+ setLastSuccessfulFetchTimestampMillis(timestamp) {
1081
+ return this.set('last_successful_fetch_timestamp_millis', timestamp);
1082
+ }
1083
+ getLastSuccessfulFetchResponse() {
1084
+ return this.get('last_successful_fetch_response');
1085
+ }
1086
+ setLastSuccessfulFetchResponse(response) {
1087
+ return this.set('last_successful_fetch_response', response);
1088
+ }
1089
+ getActiveConfig() {
1090
+ return this.get('active_config');
1091
+ }
1092
+ setActiveConfig(config) {
1093
+ return this.set('active_config', config);
1094
+ }
1095
+ getActiveConfigEtag() {
1096
+ return this.get('active_config_etag');
1097
+ }
1098
+ setActiveConfigEtag(etag) {
1099
+ return this.set('active_config_etag', etag);
1100
+ }
1101
+ getActiveExperiments() {
1102
+ return this.get('active_experiments');
1103
+ }
1104
+ setActiveExperiments(experiments) {
1105
+ return this.set('active_experiments', experiments);
1106
+ }
1107
+ getThrottleMetadata() {
1108
+ return this.get('throttle_metadata');
1109
+ }
1110
+ setThrottleMetadata(metadata) {
1111
+ return this.set('throttle_metadata', metadata);
1112
+ }
1113
+ deleteThrottleMetadata() {
1114
+ return this.delete('throttle_metadata');
1115
+ }
1116
+ getCustomSignals() {
1117
+ return this.get('custom_signals');
1118
+ }
1119
+ getRealtimeBackoffMetadata() {
1120
+ return this.get('realtime_backoff_metadata');
1121
+ }
1122
+ setRealtimeBackoffMetadata(realtimeMetadata) {
1123
+ return this.set('realtime_backoff_metadata', realtimeMetadata);
1124
+ }
1125
+ getActiveConfigTemplateVersion() {
1126
+ return this.get('last_known_template_version');
1127
+ }
1128
+ setActiveConfigTemplateVersion(version) {
1129
+ return this.set('last_known_template_version', version);
1130
+ }
1131
+ }
1132
+ class IndexedDbStorage extends Storage {
1133
+ /**
1134
+ * @param appId enables storage segmentation by app (ID + name).
1135
+ * @param appName enables storage segmentation by app (ID + name).
1136
+ * @param namespace enables storage segmentation by namespace.
1137
+ */
1138
+ constructor(appId, appName, namespace, openDbPromise = openDatabase()) {
1139
+ super();
1140
+ this.appId = appId;
1141
+ this.appName = appName;
1142
+ this.namespace = namespace;
1143
+ this.openDbPromise = openDbPromise;
1144
+ }
1145
+ async setCustomSignals(customSignals) {
1146
+ const db = await this.openDbPromise;
1147
+ const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
1148
+ const storedSignals = await this.getWithTransaction('custom_signals', transaction);
1149
+ const updatedSignals = mergeCustomSignals(customSignals, storedSignals || {});
1150
+ await this.setWithTransaction('custom_signals', updatedSignals, transaction);
1151
+ return updatedSignals;
1152
+ }
1153
+ /**
1154
+ * Gets a value from the database using the provided transaction.
1155
+ *
1156
+ * @param key The key of the value to get.
1157
+ * @param transaction The transaction to use for the operation.
1158
+ * @returns The value associated with the key, or undefined if no such value exists.
1159
+ */
1160
+ async getWithTransaction(key, transaction) {
1161
+ return new Promise((resolve, reject) => {
1162
+ const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
1163
+ const compositeKey = this.createCompositeKey(key);
1164
+ try {
1165
+ const request = objectStore.get(compositeKey);
1166
+ request.onerror = event => {
1167
+ reject(toFirebaseError(event, "storage-get" /* ErrorCode.STORAGE_GET */));
1168
+ };
1169
+ request.onsuccess = event => {
1170
+ const result = event.target.result;
1171
+ if (result) {
1172
+ resolve(result.value);
1173
+ }
1174
+ else {
1175
+ resolve(undefined);
1176
+ }
1177
+ };
1178
+ }
1179
+ catch (e) {
1180
+ reject(ERROR_FACTORY.create("storage-get" /* ErrorCode.STORAGE_GET */, {
1181
+ originalErrorMessage: e?.message
1182
+ }));
1183
+ }
1184
+ });
1185
+ }
1186
+ /**
1187
+ * Sets a value in the database using the provided transaction.
1188
+ *
1189
+ * @param key The key of the value to set.
1190
+ * @param value The value to set.
1191
+ * @param transaction The transaction to use for the operation.
1192
+ * @returns A promise that resolves when the operation is complete.
1193
+ */
1194
+ async setWithTransaction(key, value, transaction) {
1195
+ return new Promise((resolve, reject) => {
1196
+ const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
1197
+ const compositeKey = this.createCompositeKey(key);
1198
+ try {
1199
+ const request = objectStore.put({
1200
+ compositeKey,
1201
+ value
1202
+ });
1203
+ request.onerror = (event) => {
1204
+ reject(toFirebaseError(event, "storage-set" /* ErrorCode.STORAGE_SET */));
1205
+ };
1206
+ request.onsuccess = () => {
1207
+ resolve();
1208
+ };
1209
+ }
1210
+ catch (e) {
1211
+ reject(ERROR_FACTORY.create("storage-set" /* ErrorCode.STORAGE_SET */, {
1212
+ originalErrorMessage: e?.message
1213
+ }));
1214
+ }
1215
+ });
1216
+ }
1217
+ async get(key) {
1218
+ const db = await this.openDbPromise;
1219
+ const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly');
1220
+ return this.getWithTransaction(key, transaction);
1221
+ }
1222
+ async set(key, value) {
1223
+ const db = await this.openDbPromise;
1224
+ const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
1225
+ return this.setWithTransaction(key, value, transaction);
1226
+ }
1227
+ async delete(key) {
1228
+ const db = await this.openDbPromise;
1229
+ return new Promise((resolve, reject) => {
1230
+ const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
1231
+ const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
1232
+ const compositeKey = this.createCompositeKey(key);
1233
+ try {
1234
+ const request = objectStore.delete(compositeKey);
1235
+ request.onerror = (event) => {
1236
+ reject(toFirebaseError(event, "storage-delete" /* ErrorCode.STORAGE_DELETE */));
1237
+ };
1238
+ request.onsuccess = () => {
1239
+ resolve();
1240
+ };
1241
+ }
1242
+ catch (e) {
1243
+ reject(ERROR_FACTORY.create("storage-delete" /* ErrorCode.STORAGE_DELETE */, {
1244
+ originalErrorMessage: e?.message
1245
+ }));
1246
+ }
1247
+ });
1248
+ }
1249
+ // Facilitates composite key functionality (which is unsupported in IE).
1250
+ createCompositeKey(key) {
1251
+ return [this.appId, this.appName, this.namespace, key].join();
1252
+ }
1253
+ }
1254
+ class InMemoryStorage extends Storage {
1255
+ constructor() {
1256
+ super(...arguments);
1257
+ this.storage = {};
1258
+ }
1259
+ async get(key) {
1260
+ return Promise.resolve(this.storage[key]);
1261
+ }
1262
+ async set(key, value) {
1263
+ this.storage[key] = value;
1264
+ return Promise.resolve(undefined);
1265
+ }
1266
+ async delete(key) {
1267
+ this.storage[key] = undefined;
1268
+ return Promise.resolve();
1269
+ }
1270
+ async setCustomSignals(customSignals) {
1271
+ const storedSignals = (this.storage['custom_signals'] ||
1272
+ {});
1273
+ this.storage['custom_signals'] = mergeCustomSignals(customSignals, storedSignals);
1274
+ return Promise.resolve(this.storage['custom_signals']);
1275
+ }
1276
+ }
1277
+ function mergeCustomSignals(customSignals, storedSignals) {
1278
+ const combinedSignals = {
1279
+ ...storedSignals,
1280
+ ...customSignals
1281
+ };
1282
+ // Filter out key-value assignments with null values since they are signals being unset
1283
+ const updatedSignals = Object.fromEntries(Object.entries(combinedSignals)
1284
+ .filter(([_, v]) => v !== null)
1285
+ .map(([k, v]) => {
1286
+ // Stringify numbers to store a map of string keys and values which can be sent
1287
+ // as-is in a fetch call.
1288
+ if (typeof v === 'number') {
1289
+ return [k, v.toString()];
1290
+ }
1291
+ return [k, v];
1292
+ }));
1293
+ // Throw an error if the number of custom signals to be stored exceeds the limit
1294
+ if (Object.keys(updatedSignals).length > RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS) {
1295
+ throw ERROR_FACTORY.create("custom-signal-max-allowed-signals" /* ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS */, {
1296
+ maxSignals: RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS
1297
+ });
1298
+ }
1299
+ return updatedSignals;
1300
+ }
1301
+
1302
+ /**
1303
+ * @license
1304
+ * Copyright 2019 Google LLC
1305
+ *
1306
+ * Licensed under the Apache License, Version 2.0 (the "License");
1307
+ * you may not use this file except in compliance with the License.
1308
+ * You may obtain a copy of the License at
1309
+ *
1310
+ * http://www.apache.org/licenses/LICENSE-2.0
1311
+ *
1312
+ * Unless required by applicable law or agreed to in writing, software
1313
+ * distributed under the License is distributed on an "AS IS" BASIS,
1314
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1315
+ * See the License for the specific language governing permissions and
1316
+ * limitations under the License.
1317
+ */
1318
+ /**
1319
+ * A memory cache layer over storage to support the SDK's synchronous read requirements.
1320
+ */
1321
+ class StorageCache {
1322
+ constructor(storage) {
1323
+ this.storage = storage;
1324
+ }
1325
+ /**
1326
+ * Memory-only getters
1327
+ */
1328
+ getLastFetchStatus() {
1329
+ return this.lastFetchStatus;
1330
+ }
1331
+ getLastSuccessfulFetchTimestampMillis() {
1332
+ return this.lastSuccessfulFetchTimestampMillis;
1333
+ }
1334
+ getActiveConfig() {
1335
+ return this.activeConfig;
1336
+ }
1337
+ getCustomSignals() {
1338
+ return this.customSignals;
1339
+ }
1340
+ /**
1341
+ * Read-ahead getter
1342
+ */
1343
+ async loadFromStorage() {
1344
+ const lastFetchStatusPromise = this.storage.getLastFetchStatus();
1345
+ const lastSuccessfulFetchTimestampMillisPromise = this.storage.getLastSuccessfulFetchTimestampMillis();
1346
+ const activeConfigPromise = this.storage.getActiveConfig();
1347
+ const customSignalsPromise = this.storage.getCustomSignals();
1348
+ // Note:
1349
+ // 1. we consistently check for undefined to avoid clobbering defined values
1350
+ // in memory
1351
+ // 2. we defer awaiting to improve readability, as opposed to destructuring
1352
+ // a Promise.all result, for example
1353
+ const lastFetchStatus = await lastFetchStatusPromise;
1354
+ if (lastFetchStatus) {
1355
+ this.lastFetchStatus = lastFetchStatus;
1356
+ }
1357
+ const lastSuccessfulFetchTimestampMillis = await lastSuccessfulFetchTimestampMillisPromise;
1358
+ if (lastSuccessfulFetchTimestampMillis) {
1359
+ this.lastSuccessfulFetchTimestampMillis =
1360
+ lastSuccessfulFetchTimestampMillis;
1361
+ }
1362
+ const activeConfig = await activeConfigPromise;
1363
+ if (activeConfig) {
1364
+ this.activeConfig = activeConfig;
1365
+ }
1366
+ const customSignals = await customSignalsPromise;
1367
+ if (customSignals) {
1368
+ this.customSignals = customSignals;
1369
+ }
1370
+ }
1371
+ /**
1372
+ * Write-through setters
1373
+ */
1374
+ setLastFetchStatus(status) {
1375
+ this.lastFetchStatus = status;
1376
+ return this.storage.setLastFetchStatus(status);
1377
+ }
1378
+ setLastSuccessfulFetchTimestampMillis(timestampMillis) {
1379
+ this.lastSuccessfulFetchTimestampMillis = timestampMillis;
1380
+ return this.storage.setLastSuccessfulFetchTimestampMillis(timestampMillis);
1381
+ }
1382
+ setActiveConfig(activeConfig) {
1383
+ this.activeConfig = activeConfig;
1384
+ return this.storage.setActiveConfig(activeConfig);
1385
+ }
1386
+ async setCustomSignals(customSignals) {
1387
+ this.customSignals = await this.storage.setCustomSignals(customSignals);
1388
+ }
1389
+ }
1390
+
1391
+ /**
1392
+ * @license
1393
+ * Copyright 2025 Google LLC
1394
+ *
1395
+ * Licensed under the Apache License, Version 2.0 (the "License");
1396
+ * you may not use this file except in compliance with the License.
1397
+ * You may obtain a copy of the License at
1398
+ *
1399
+ * http://www.apache.org/licenses/LICENSE-2.0
1400
+ *
1401
+ * Unless required by applicable law or agreed to in writing, software
1402
+ * distributed under the License is distributed on an "AS IS" BASIS,
1403
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1404
+ * See the License for the specific language governing permissions and
1405
+ * limitations under the License.
1406
+ */
1407
+ // 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.
1408
+ /**
1409
+ * Base class to be used if you want to emit events. Call the constructor with
1410
+ * the set of allowed event names.
1411
+ */
1412
+ class EventEmitter {
1413
+ constructor(allowedEvents_) {
1414
+ this.allowedEvents_ = allowedEvents_;
1415
+ this.listeners_ = {};
1416
+ assert(Array.isArray(allowedEvents_) && allowedEvents_.length > 0, 'Requires a non-empty array');
1417
+ }
1418
+ /**
1419
+ * To be called by derived classes to trigger events.
1420
+ */
1421
+ trigger(eventType, ...varArgs) {
1422
+ if (Array.isArray(this.listeners_[eventType])) {
1423
+ // Clone the list, since callbacks could add/remove listeners.
1424
+ const listeners = [...this.listeners_[eventType]];
1425
+ for (let i = 0; i < listeners.length; i++) {
1426
+ listeners[i].callback.apply(listeners[i].context, varArgs);
1427
+ }
1428
+ }
1429
+ }
1430
+ on(eventType, callback, context) {
1431
+ this.validateEventType_(eventType);
1432
+ this.listeners_[eventType] = this.listeners_[eventType] || [];
1433
+ this.listeners_[eventType].push({ callback, context });
1434
+ const eventData = this.getInitialEvent(eventType);
1435
+ if (eventData) {
1436
+ //@ts-ignore
1437
+ callback.apply(context, eventData);
1438
+ }
1439
+ }
1440
+ off(eventType, callback, context) {
1441
+ this.validateEventType_(eventType);
1442
+ const listeners = this.listeners_[eventType] || [];
1443
+ for (let i = 0; i < listeners.length; i++) {
1444
+ if (listeners[i].callback === callback &&
1445
+ (!context || context === listeners[i].context)) {
1446
+ listeners.splice(i, 1);
1447
+ return;
1448
+ }
1449
+ }
1450
+ }
1451
+ validateEventType_(eventType) {
1452
+ assert(this.allowedEvents_.find(et => {
1453
+ return et === eventType;
1454
+ }), 'Unknown event: ' + eventType);
1455
+ }
1456
+ }
1457
+
1458
+ /**
1459
+ * @license
1460
+ * Copyright 2025 Google LLC
1461
+ *
1462
+ * Licensed under the Apache License, Version 2.0 (the "License");
1463
+ * you may not use this file except in compliance with the License.
1464
+ * You may obtain a copy of the License at
1465
+ *
1466
+ * http://www.apache.org/licenses/LICENSE-2.0
1467
+ *
1468
+ * Unless required by applicable law or agreed to in writing, software
1469
+ * distributed under the License is distributed on an "AS IS" BASIS,
1470
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1471
+ * See the License for the specific language governing permissions and
1472
+ * limitations under the License.
1473
+ */
1474
+ // 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.
1475
+ class VisibilityMonitor extends EventEmitter {
1476
+ static getInstance() {
1477
+ return new VisibilityMonitor();
1478
+ }
1479
+ constructor() {
1480
+ super(['visible']);
1481
+ let hidden;
1482
+ let visibilityChange;
1483
+ if (typeof document !== 'undefined' &&
1484
+ typeof document.addEventListener !== 'undefined') {
1485
+ if (typeof document['hidden'] !== 'undefined') {
1486
+ // Opera 12.10 and Firefox 18 and later support
1487
+ visibilityChange = 'visibilitychange';
1488
+ hidden = 'hidden';
1489
+ } // @ts-ignore
1490
+ else if (typeof document['mozHidden'] !== 'undefined') {
1491
+ visibilityChange = 'mozvisibilitychange';
1492
+ hidden = 'mozHidden';
1493
+ } // @ts-ignore
1494
+ else if (typeof document['msHidden'] !== 'undefined') {
1495
+ visibilityChange = 'msvisibilitychange';
1496
+ hidden = 'msHidden';
1497
+ } // @ts-ignore
1498
+ else if (typeof document['webkitHidden'] !== 'undefined') {
1499
+ visibilityChange = 'webkitvisibilitychange';
1500
+ hidden = 'webkitHidden';
1501
+ }
1502
+ }
1503
+ // Initially, we always assume we are visible. This ensures that in browsers
1504
+ // without page visibility support or in cases where we are never visible
1505
+ // (e.g. chrome extension), we act as if we are visible, i.e. don't delay
1506
+ // reconnects
1507
+ this.visible_ = true;
1508
+ // @ts-ignore
1509
+ if (visibilityChange) {
1510
+ document.addEventListener(visibilityChange, () => {
1511
+ // @ts-ignore
1512
+ const visible = !document[hidden];
1513
+ if (visible !== this.visible_) {
1514
+ this.visible_ = visible;
1515
+ this.trigger('visible', visible);
1516
+ }
1517
+ }, false);
1518
+ }
1519
+ }
1520
+ getInitialEvent(eventType) {
1521
+ assert(eventType === 'visible', 'Unknown event type: ' + eventType);
1522
+ return [this.visible_];
1523
+ }
1524
+ }
1525
+
1526
+ /**
1527
+ * @license
1528
+ * Copyright 2025 Google LLC
1529
+ *
1530
+ * Licensed under the Apache License, Version 2.0 (the "License");
1531
+ * you may not use this file except in compliance with the License.
1532
+ * You may obtain a copy of the License at
1533
+ *
1534
+ * http://www.apache.org/licenses/LICENSE-2.0
1535
+ *
1536
+ * Unless required by applicable law or agreed to in writing, software
1537
+ * distributed under the License is distributed on an "AS IS" BASIS,
1538
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1539
+ * See the License for the specific language governing permissions and
1540
+ * limitations under the License.
1541
+ */
1542
+ const API_KEY_HEADER = 'X-Goog-Api-Key';
1543
+ const INSTALLATIONS_AUTH_TOKEN_HEADER = 'X-Goog-Firebase-Installations-Auth';
1544
+ const ORIGINAL_RETRIES = 8;
1545
+ const MAXIMUM_FETCH_ATTEMPTS = 3;
1546
+ const NO_BACKOFF_TIME_IN_MILLIS = -1;
1547
+ const NO_FAILED_REALTIME_STREAMS = 0;
1548
+ const REALTIME_DISABLED_KEY = 'featureDisabled';
1549
+ const REALTIME_RETRY_INTERVAL = 'retryIntervalSeconds';
1550
+ const TEMPLATE_VERSION_KEY = 'latestTemplateVersionNumber';
1551
+ class RealtimeHandler {
1552
+ constructor(firebaseInstallations, storage, sdkVersion, namespace, projectId, apiKey, appId, logger, storageCache, cachingClient) {
1553
+ this.firebaseInstallations = firebaseInstallations;
1554
+ this.storage = storage;
1555
+ this.sdkVersion = sdkVersion;
1556
+ this.namespace = namespace;
1557
+ this.projectId = projectId;
1558
+ this.apiKey = apiKey;
1559
+ this.appId = appId;
1560
+ this.logger = logger;
1561
+ this.storageCache = storageCache;
1562
+ this.cachingClient = cachingClient;
1563
+ this.observers = new Set();
1564
+ this.isConnectionActive = false;
1565
+ this.isRealtimeDisabled = false;
1566
+ this.httpRetriesRemaining = ORIGINAL_RETRIES;
1567
+ this.isInBackground = false;
1568
+ this.decoder = new TextDecoder('utf-8');
1569
+ this.isClosingConnection = false;
1570
+ this.propagateError = (e) => this.observers.forEach(o => o.error?.(e));
1571
+ /**
1572
+ * HTTP status code that the Realtime client should retry on.
1573
+ */
1574
+ this.isStatusCodeRetryable = (statusCode) => {
1575
+ const retryableStatusCodes = [
1576
+ 408, // Request Timeout
1577
+ 429, // Too Many Requests
1578
+ 502, // Bad Gateway
1579
+ 503, // Service Unavailable
1580
+ 504 // Gateway Timeout
1581
+ ];
1582
+ return !statusCode || retryableStatusCodes.includes(statusCode);
1583
+ };
1584
+ void this.setRetriesRemaining();
1585
+ void VisibilityMonitor.getInstance().on('visible', this.onVisibilityChange, this);
1586
+ }
1587
+ async setRetriesRemaining() {
1588
+ // Retrieve number of remaining retries from last session. The minimum retry count being one.
1589
+ const metadata = await this.storage.getRealtimeBackoffMetadata();
1590
+ const numFailedStreams = metadata?.numFailedStreams || 0;
1591
+ this.httpRetriesRemaining = Math.max(ORIGINAL_RETRIES - numFailedStreams, 1);
1592
+ }
1593
+ /**
1594
+ * Increment the number of failed stream attempts, increase the backoff duration, set the backoff
1595
+ * end time to "backoff duration" after `lastFailedStreamTime` and persist the new
1596
+ * values to storage metadata.
1597
+ */
1598
+ async updateBackoffMetadataWithLastFailedStreamConnectionTime(lastFailedStreamTime) {
1599
+ const numFailedStreams = ((await this.storage.getRealtimeBackoffMetadata())?.numFailedStreams ||
1600
+ 0) + 1;
1601
+ const backoffMillis = calculateBackoffMillis(numFailedStreams, 60000, 2);
1602
+ await this.storage.setRealtimeBackoffMetadata({
1603
+ backoffEndTimeMillis: new Date(lastFailedStreamTime.getTime() + backoffMillis),
1604
+ numFailedStreams
1605
+ });
1606
+ }
1607
+ /**
1608
+ * Increase the backoff duration with a new end time based on Retry Interval.
1609
+ */
1610
+ async updateBackoffMetadataWithRetryInterval(retryIntervalSeconds) {
1611
+ const currentTime = Date.now();
1612
+ const backoffDurationInMillis = retryIntervalSeconds * 1000;
1613
+ const backoffEndTime = new Date(currentTime + backoffDurationInMillis);
1614
+ const numFailedStreams = 0;
1615
+ await this.storage.setRealtimeBackoffMetadata({
1616
+ backoffEndTimeMillis: backoffEndTime,
1617
+ numFailedStreams
1618
+ });
1619
+ await this.retryHttpConnectionWhenBackoffEnds();
1620
+ }
1621
+ /**
1622
+ * Closes the realtime HTTP connection.
1623
+ * Note: This method is designed to be called only once at a time.
1624
+ * If a call is already in progress, subsequent calls will be ignored.
1625
+ */
1626
+ async closeRealtimeHttpConnection() {
1627
+ if (this.isClosingConnection) {
1628
+ return;
1629
+ }
1630
+ this.isClosingConnection = true;
1631
+ try {
1632
+ if (this.reader) {
1633
+ await this.reader.cancel();
1634
+ }
1635
+ }
1636
+ catch (e) {
1637
+ // The network connection was lost, so cancel() failed.
1638
+ // This is expected in a disconnected state, so we can safely ignore the error.
1639
+ this.logger.debug('Failed to cancel the reader, connection was lost.');
1640
+ }
1641
+ finally {
1642
+ this.reader = undefined;
1643
+ }
1644
+ if (this.controller) {
1645
+ await this.controller.abort();
1646
+ this.controller = undefined;
1647
+ }
1648
+ this.isClosingConnection = false;
1649
+ }
1650
+ async resetRealtimeBackoff() {
1651
+ await this.storage.setRealtimeBackoffMetadata({
1652
+ backoffEndTimeMillis: new Date(-1),
1653
+ numFailedStreams: 0
1654
+ });
1655
+ }
1656
+ resetRetryCount() {
1657
+ this.httpRetriesRemaining = ORIGINAL_RETRIES;
1658
+ }
1659
+ /**
1660
+ * Assembles the request headers and body and executes the fetch request to
1661
+ * establish the real-time streaming connection. This is the "worker" method
1662
+ * that performs the actual network communication.
1663
+ */
1664
+ async establishRealtimeConnection(url, installationId, installationTokenResult, signal) {
1665
+ const eTagValue = await this.storage.getActiveConfigEtag();
1666
+ const lastKnownVersionNumber = await this.storage.getActiveConfigTemplateVersion();
1667
+ const headers = {
1668
+ [API_KEY_HEADER]: this.apiKey,
1669
+ [INSTALLATIONS_AUTH_TOKEN_HEADER]: installationTokenResult,
1670
+ 'Content-Type': 'application/json',
1671
+ 'Accept': 'application/json',
1672
+ 'If-None-Match': eTagValue || '*',
1673
+ 'Content-Encoding': 'gzip'
1674
+ };
1675
+ const requestBody = {
1676
+ project: this.projectId,
1677
+ namespace: this.namespace,
1678
+ lastKnownVersionNumber,
1679
+ appId: this.appId,
1680
+ sdkVersion: this.sdkVersion,
1681
+ appInstanceId: installationId
1682
+ };
1683
+ const response = await fetch(url, {
1684
+ method: 'POST',
1685
+ headers,
1686
+ body: JSON.stringify(requestBody),
1687
+ signal
1688
+ });
1689
+ return response;
1690
+ }
1691
+ getRealtimeUrl() {
1692
+ const urlBase = window.FIREBASE_REMOTE_CONFIG_URL_BASE ||
1693
+ 'https://firebaseremoteconfigrealtime.googleapis.com';
1694
+ const urlString = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:streamFetchInvalidations?key=${this.apiKey}`;
1695
+ return new URL(urlString);
1696
+ }
1697
+ async createRealtimeConnection() {
1698
+ const [installationId, installationTokenResult] = await Promise.all([
1699
+ this.firebaseInstallations.getId(),
1700
+ this.firebaseInstallations.getToken(false)
1701
+ ]);
1702
+ this.controller = new AbortController();
1703
+ const url = this.getRealtimeUrl();
1704
+ const realtimeConnection = await this.establishRealtimeConnection(url, installationId, installationTokenResult, this.controller.signal);
1705
+ return realtimeConnection;
1706
+ }
1707
+ /**
1708
+ * Retries HTTP stream connection asyncly in random time intervals.
1709
+ */
1710
+ async retryHttpConnectionWhenBackoffEnds() {
1711
+ let backoffMetadata = await this.storage.getRealtimeBackoffMetadata();
1712
+ if (!backoffMetadata) {
1713
+ backoffMetadata = {
1714
+ backoffEndTimeMillis: new Date(NO_BACKOFF_TIME_IN_MILLIS),
1715
+ numFailedStreams: NO_FAILED_REALTIME_STREAMS
1716
+ };
1717
+ }
1718
+ const backoffEndTime = new Date(backoffMetadata.backoffEndTimeMillis).getTime();
1719
+ const currentTime = Date.now();
1720
+ const retryMillis = Math.max(0, backoffEndTime - currentTime);
1721
+ await this.makeRealtimeHttpConnection(retryMillis);
1722
+ }
1723
+ setIsHttpConnectionRunning(connectionRunning) {
1724
+ this.isConnectionActive = connectionRunning;
1725
+ }
1726
+ /**
1727
+ * Combines the check and set operations to prevent multiple asynchronous
1728
+ * calls from redundantly starting an HTTP connection. This ensures that
1729
+ * only one attempt is made at a time.
1730
+ */
1731
+ checkAndSetHttpConnectionFlagIfNotRunning() {
1732
+ const canMakeConnection = this.canEstablishStreamConnection();
1733
+ if (canMakeConnection) {
1734
+ this.setIsHttpConnectionRunning(true);
1735
+ }
1736
+ return canMakeConnection;
1737
+ }
1738
+ fetchResponseIsUpToDate(fetchResponse, lastKnownVersion) {
1739
+ // If there is a config, make sure its version is >= the last known version.
1740
+ if (fetchResponse.config != null && fetchResponse.templateVersion) {
1741
+ return fetchResponse.templateVersion >= lastKnownVersion;
1742
+ }
1743
+ // If there isn't a config, return true if the fetch was successful and backend had no update.
1744
+ // Else, it returned an out of date config.
1745
+ return this.storageCache.getLastFetchStatus() === 'success';
1746
+ }
1747
+ parseAndValidateConfigUpdateMessage(message) {
1748
+ const left = message.indexOf('{');
1749
+ const right = message.indexOf('}', left);
1750
+ if (left < 0 || right < 0) {
1751
+ return '';
1752
+ }
1753
+ return left >= right ? '' : message.substring(left, right + 1);
1754
+ }
1755
+ isEventListenersEmpty() {
1756
+ return this.observers.size === 0;
1757
+ }
1758
+ getRandomInt(max) {
1759
+ return Math.floor(Math.random() * max);
1760
+ }
1761
+ executeAllListenerCallbacks(configUpdate) {
1762
+ this.observers.forEach(observer => observer.next(configUpdate));
1763
+ }
1764
+ /**
1765
+ * Compares two configuration objects and returns a set of keys that have changed.
1766
+ * A key is considered changed if it's new, removed, or has a different value.
1767
+ */
1768
+ getChangedParams(newConfig, oldConfig) {
1769
+ const changedKeys = new Set();
1770
+ const newKeys = new Set(Object.keys(newConfig || {}));
1771
+ const oldKeys = new Set(Object.keys(oldConfig || {}));
1772
+ for (const key of newKeys) {
1773
+ if (!oldKeys.has(key) || newConfig[key] !== oldConfig[key]) {
1774
+ changedKeys.add(key);
1775
+ }
1776
+ }
1777
+ for (const key of oldKeys) {
1778
+ if (!newKeys.has(key)) {
1779
+ changedKeys.add(key);
1780
+ }
1781
+ }
1782
+ return changedKeys;
1783
+ }
1784
+ async fetchLatestConfig(remainingAttempts, targetVersion) {
1785
+ const remainingAttemptsAfterFetch = remainingAttempts - 1;
1786
+ const currentAttempt = MAXIMUM_FETCH_ATTEMPTS - remainingAttemptsAfterFetch;
1787
+ const customSignals = this.storageCache.getCustomSignals();
1788
+ if (customSignals) {
1789
+ this.logger.debug(`Fetching config with custom signals: ${JSON.stringify(customSignals)}`);
1790
+ }
1791
+ const abortSignal = new RemoteConfigAbortSignal();
1792
+ try {
1793
+ const fetchRequest = {
1794
+ cacheMaxAgeMillis: 0,
1795
+ signal: abortSignal,
1796
+ customSignals,
1797
+ fetchType: 'REALTIME',
1798
+ fetchAttempt: currentAttempt
1799
+ };
1800
+ const fetchResponse = await this.cachingClient.fetch(fetchRequest);
1801
+ let activatedConfigs = await this.storage.getActiveConfig();
1802
+ if (!this.fetchResponseIsUpToDate(fetchResponse, targetVersion)) {
1803
+ this.logger.debug("Fetched template version is the same as SDK's current version." +
1804
+ ' Retrying fetch.');
1805
+ // Continue fetching until template version number is greater than current.
1806
+ await this.autoFetch(remainingAttemptsAfterFetch, targetVersion);
1807
+ return;
1808
+ }
1809
+ if (fetchResponse.config == null) {
1810
+ this.logger.debug('The fetch succeeded, but the backend had no updates.');
1811
+ return;
1812
+ }
1813
+ if (activatedConfigs == null) {
1814
+ activatedConfigs = {};
1815
+ }
1816
+ const updatedKeys = this.getChangedParams(fetchResponse.config, activatedConfigs);
1817
+ if (updatedKeys.size === 0) {
1818
+ this.logger.debug('Config was fetched, but no params changed.');
1819
+ return;
1820
+ }
1821
+ const configUpdate = {
1822
+ getUpdatedKeys() {
1823
+ return new Set(updatedKeys);
1824
+ }
1825
+ };
1826
+ this.executeAllListenerCallbacks(configUpdate);
1827
+ }
1828
+ catch (e) {
1829
+ const errorMessage = e instanceof Error ? e.message : String(e);
1830
+ const error = ERROR_FACTORY.create("update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */, {
1831
+ originalErrorMessage: `Failed to auto-fetch config update: ${errorMessage}`
1832
+ });
1833
+ this.propagateError(error);
1834
+ }
1835
+ }
1836
+ async autoFetch(remainingAttempts, targetVersion) {
1837
+ if (remainingAttempts === 0) {
1838
+ const error = ERROR_FACTORY.create("update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */, {
1839
+ originalErrorMessage: 'Unable to fetch the latest version of the template.'
1840
+ });
1841
+ this.propagateError(error);
1842
+ return;
1843
+ }
1844
+ const timeTillFetchSeconds = this.getRandomInt(4);
1845
+ const timeTillFetchInMiliseconds = timeTillFetchSeconds * 1000;
1846
+ await new Promise(resolve => setTimeout(resolve, timeTillFetchInMiliseconds));
1847
+ await this.fetchLatestConfig(remainingAttempts, targetVersion);
1848
+ }
1849
+ /**
1850
+ * Processes a stream of real-time messages for configuration updates.
1851
+ * This method reassembles fragmented messages, validates and parses the JSON,
1852
+ * and automatically fetches a new config if a newer template version is available.
1853
+ * It also handles server-specified retry intervals and propagates errors for
1854
+ * invalid messages or when real-time updates are disabled.
1855
+ */
1856
+ async handleNotifications(reader) {
1857
+ let partialConfigUpdateMessage;
1858
+ let currentConfigUpdateMessage = '';
1859
+ while (true) {
1860
+ const { done, value } = await reader.read();
1861
+ if (done) {
1862
+ break;
1863
+ }
1864
+ partialConfigUpdateMessage = this.decoder.decode(value, { stream: true });
1865
+ currentConfigUpdateMessage += partialConfigUpdateMessage;
1866
+ if (partialConfigUpdateMessage.includes('}')) {
1867
+ currentConfigUpdateMessage = this.parseAndValidateConfigUpdateMessage(currentConfigUpdateMessage);
1868
+ if (currentConfigUpdateMessage.length === 0) {
1869
+ continue;
1870
+ }
1871
+ try {
1872
+ const jsonObject = JSON.parse(currentConfigUpdateMessage);
1873
+ if (this.isEventListenersEmpty()) {
1874
+ break;
1875
+ }
1876
+ if (REALTIME_DISABLED_KEY in jsonObject &&
1877
+ jsonObject[REALTIME_DISABLED_KEY] === true) {
1878
+ const error = ERROR_FACTORY.create("realtime-unavailable" /* ErrorCode.CONFIG_UPDATE_UNAVAILABLE */, {
1879
+ originalErrorMessage: 'The server is temporarily unavailable. Try again in a few minutes.'
1880
+ });
1881
+ this.propagateError(error);
1882
+ break;
1883
+ }
1884
+ if (TEMPLATE_VERSION_KEY in jsonObject) {
1885
+ const oldTemplateVersion = await this.storage.getActiveConfigTemplateVersion();
1886
+ const targetTemplateVersion = Number(jsonObject[TEMPLATE_VERSION_KEY]);
1887
+ if (oldTemplateVersion &&
1888
+ targetTemplateVersion > oldTemplateVersion) {
1889
+ await this.autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion);
1890
+ }
1891
+ }
1892
+ // This field in the response indicates that the realtime request should retry after the
1893
+ // specified interval to establish a long-lived connection. This interval extends the
1894
+ // backoff duration without affecting the number of retries, so it will not enter an
1895
+ // exponential backoff state.
1896
+ if (REALTIME_RETRY_INTERVAL in jsonObject) {
1897
+ const retryIntervalSeconds = Number(jsonObject[REALTIME_RETRY_INTERVAL]);
1898
+ await this.updateBackoffMetadataWithRetryInterval(retryIntervalSeconds);
1899
+ }
1900
+ }
1901
+ catch (e) {
1902
+ this.logger.debug('Unable to parse latest config update message.', e);
1903
+ const errorMessage = e instanceof Error ? e.message : String(e);
1904
+ this.propagateError(ERROR_FACTORY.create("update-message-invalid" /* ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID */, {
1905
+ originalErrorMessage: errorMessage
1906
+ }));
1907
+ }
1908
+ currentConfigUpdateMessage = '';
1909
+ }
1910
+ }
1911
+ }
1912
+ async listenForNotifications(reader) {
1913
+ try {
1914
+ await this.handleNotifications(reader);
1915
+ }
1916
+ catch (e) {
1917
+ // If the real-time connection is at an unexpected lifecycle state when the app is
1918
+ // backgrounded, it's expected closing the connection will throw an exception.
1919
+ if (!this.isInBackground) {
1920
+ // Otherwise, the real-time server connection was closed due to a transient issue.
1921
+ this.logger.debug('Real-time connection was closed due to an exception.');
1922
+ }
1923
+ }
1924
+ }
1925
+ /**
1926
+ * Open the real-time connection, begin listening for updates, and auto-fetch when an update is
1927
+ * received.
1928
+ *
1929
+ * If the connection is successful, this method will block on its thread while it reads the
1930
+ * chunk-encoded HTTP body. When the connection closes, it attempts to reestablish the stream.
1931
+ */
1932
+ async prepareAndBeginRealtimeHttpStream() {
1933
+ if (!this.checkAndSetHttpConnectionFlagIfNotRunning()) {
1934
+ return;
1935
+ }
1936
+ let backoffMetadata = await this.storage.getRealtimeBackoffMetadata();
1937
+ if (!backoffMetadata) {
1938
+ backoffMetadata = {
1939
+ backoffEndTimeMillis: new Date(NO_BACKOFF_TIME_IN_MILLIS),
1940
+ numFailedStreams: NO_FAILED_REALTIME_STREAMS
1941
+ };
1942
+ }
1943
+ const backoffEndTime = backoffMetadata.backoffEndTimeMillis.getTime();
1944
+ if (Date.now() < backoffEndTime) {
1945
+ await this.retryHttpConnectionWhenBackoffEnds();
1946
+ return;
1947
+ }
1948
+ let response;
1949
+ let responseCode;
1950
+ try {
1951
+ response = await this.createRealtimeConnection();
1952
+ responseCode = response.status;
1953
+ if (response.ok && response.body) {
1954
+ this.resetRetryCount();
1955
+ await this.resetRealtimeBackoff();
1956
+ const reader = response.body.getReader();
1957
+ this.reader = reader;
1958
+ // Start listening for realtime notifications.
1959
+ await this.listenForNotifications(reader);
1960
+ }
1961
+ }
1962
+ catch (error) {
1963
+ if (this.isInBackground) {
1964
+ // It's possible the app was backgrounded while the connection was open, which
1965
+ // threw an exception trying to read the response. No real error here, so treat
1966
+ // this as a success, even if we haven't read a 200 response code yet.
1967
+ this.resetRetryCount();
1968
+ }
1969
+ else {
1970
+ //there might have been a transient error so the client will retry the connection.
1971
+ this.logger.debug('Exception connecting to real-time RC backend. Retrying the connection...:', error);
1972
+ }
1973
+ }
1974
+ finally {
1975
+ // Close HTTP connection and associated streams.
1976
+ await this.closeRealtimeHttpConnection();
1977
+ this.setIsHttpConnectionRunning(false);
1978
+ // Update backoff metadata if the connection failed in the foreground.
1979
+ const connectionFailed = !this.isInBackground &&
1980
+ (responseCode === undefined ||
1981
+ this.isStatusCodeRetryable(responseCode));
1982
+ if (connectionFailed) {
1983
+ await this.updateBackoffMetadataWithLastFailedStreamConnectionTime(new Date());
1984
+ }
1985
+ // If responseCode is null then no connection was made to server and the SDK should still retry.
1986
+ if (connectionFailed || response?.ok) {
1987
+ await this.retryHttpConnectionWhenBackoffEnds();
1988
+ }
1989
+ else {
1990
+ const errorMessage = `Unable to connect to the server. HTTP status code: ${responseCode}`;
1991
+ const firebaseError = ERROR_FACTORY.create("stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */, {
1992
+ originalErrorMessage: errorMessage
1993
+ });
1994
+ this.propagateError(firebaseError);
1995
+ }
1996
+ }
1997
+ }
1998
+ /**
1999
+ * Checks whether connection can be made or not based on some conditions
2000
+ * @returns booelean
2001
+ */
2002
+ canEstablishStreamConnection() {
2003
+ const hasActiveListeners = this.observers.size > 0;
2004
+ const isNotDisabled = !this.isRealtimeDisabled;
2005
+ const isNoConnectionActive = !this.isConnectionActive;
2006
+ const inForeground = !this.isInBackground;
2007
+ return (hasActiveListeners &&
2008
+ isNotDisabled &&
2009
+ isNoConnectionActive &&
2010
+ inForeground);
2011
+ }
2012
+ async makeRealtimeHttpConnection(delayMillis) {
2013
+ if (!this.canEstablishStreamConnection()) {
2014
+ return;
2015
+ }
2016
+ if (this.httpRetriesRemaining > 0) {
2017
+ this.httpRetriesRemaining--;
2018
+ await new Promise(resolve => setTimeout(resolve, delayMillis));
2019
+ void this.prepareAndBeginRealtimeHttpStream();
2020
+ }
2021
+ else if (!this.isInBackground) {
2022
+ const error = ERROR_FACTORY.create("stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */, {
2023
+ originalErrorMessage: 'Unable to connect to the server. Check your connection and try again.'
2024
+ });
2025
+ this.propagateError(error);
2026
+ }
2027
+ }
2028
+ async beginRealtime() {
2029
+ if (this.observers.size > 0) {
2030
+ await this.makeRealtimeHttpConnection(0);
2031
+ }
2032
+ }
2033
+ /**
2034
+ * Adds an observer to the realtime updates.
2035
+ * @param observer The observer to add.
2036
+ */
2037
+ addObserver(observer) {
2038
+ this.observers.add(observer);
2039
+ void this.beginRealtime();
2040
+ }
2041
+ /**
2042
+ * Removes an observer from the realtime updates.
2043
+ * @param observer The observer to remove.
2044
+ */
2045
+ removeObserver(observer) {
2046
+ if (this.observers.has(observer)) {
2047
+ this.observers.delete(observer);
2048
+ }
2049
+ }
2050
+ /**
2051
+ * Handles changes to the application's visibility state, managing the real-time connection.
2052
+ *
2053
+ * When the application is moved to the background, this method closes the existing
2054
+ * real-time connection to save resources. When the application returns to the
2055
+ * foreground, it attempts to re-establish the connection.
2056
+ */
2057
+ async onVisibilityChange(visible) {
2058
+ this.isInBackground = !visible;
2059
+ if (!visible) {
2060
+ await this.closeRealtimeHttpConnection();
2061
+ }
2062
+ else if (visible) {
2063
+ await this.beginRealtime();
2064
+ }
2065
+ }
2066
+ }
2067
+
2068
+ /**
2069
+ * @license
2070
+ * Copyright 2020 Google LLC
2071
+ *
2072
+ * Licensed under the Apache License, Version 2.0 (the "License");
2073
+ * you may not use this file except in compliance with the License.
2074
+ * You may obtain a copy of the License at
2075
+ *
2076
+ * http://www.apache.org/licenses/LICENSE-2.0
2077
+ *
2078
+ * Unless required by applicable law or agreed to in writing, software
2079
+ * distributed under the License is distributed on an "AS IS" BASIS,
2080
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2081
+ * See the License for the specific language governing permissions and
2082
+ * limitations under the License.
2083
+ */
2084
+ function registerRemoteConfig() {
2085
+ _registerComponent(new Component(RC_COMPONENT_NAME, remoteConfigFactory, "PUBLIC" /* ComponentType.PUBLIC */).setMultipleInstances(true));
2086
+ registerVersion(name, version);
2087
+ // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation
2088
+ registerVersion(name, version, 'esm2020');
2089
+ function remoteConfigFactory(container, { options }) {
2090
+ /* Dependencies */
2091
+ // getImmediate for FirebaseApp will always succeed
2092
+ const app = container.getProvider('app').getImmediate();
2093
+ // The following call will always succeed because rc has `import '@firebase/installations'`
2094
+ const installations = container
2095
+ .getProvider('installations-internal')
2096
+ .getImmediate();
2097
+ const analyticsProvider = container.getProvider('analytics-internal');
2098
+ // Normalizes optional inputs.
2099
+ const { projectId, apiKey, appId } = app.options;
2100
+ if (!projectId) {
2101
+ throw ERROR_FACTORY.create("registration-project-id" /* ErrorCode.REGISTRATION_PROJECT_ID */);
2102
+ }
2103
+ if (!apiKey) {
2104
+ throw ERROR_FACTORY.create("registration-api-key" /* ErrorCode.REGISTRATION_API_KEY */);
2105
+ }
2106
+ if (!appId) {
2107
+ throw ERROR_FACTORY.create("registration-app-id" /* ErrorCode.REGISTRATION_APP_ID */);
2108
+ }
2109
+ const namespace = options?.templateId || 'firebase';
2110
+ const storage = isIndexedDBAvailable()
2111
+ ? new IndexedDbStorage(appId, app.name, namespace)
2112
+ : new InMemoryStorage();
2113
+ const storageCache = new StorageCache(storage);
2114
+ const logger = new Logger(name);
2115
+ // Sets ERROR as the default log level.
2116
+ // See RemoteConfig#setLogLevel for corresponding normalization to ERROR log level.
2117
+ logger.logLevel = LogLevel.ERROR;
2118
+ const restClient = new RestClient(installations,
2119
+ // Uses the JS SDK version, by which the RC package version can be deduced, if necessary.
2120
+ SDK_VERSION, namespace, projectId, apiKey, appId);
2121
+ const retryingClient = new RetryingClient(restClient, storage);
2122
+ const cachingClient = new CachingClient(retryingClient, storage, storageCache, logger);
2123
+ const realtimeHandler = new RealtimeHandler(installations, storage, SDK_VERSION, namespace, projectId, apiKey, appId, logger, storageCache, cachingClient);
2124
+ const remoteConfigInstance = new RemoteConfig(app, cachingClient, storageCache, storage, logger, realtimeHandler, analyticsProvider);
2125
+ // Starts warming cache.
2126
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
2127
+ ensureInitialized(remoteConfigInstance);
2128
+ return remoteConfigInstance;
2129
+ }
2130
+ }
2131
+
2132
+ /**
2133
+ * @license
2134
+ * Copyright 2020 Google LLC
2135
+ *
2136
+ * Licensed under the Apache License, Version 2.0 (the "License");
2137
+ * you may not use this file except in compliance with the License.
2138
+ * You may obtain a copy of the License at
2139
+ *
2140
+ * http://www.apache.org/licenses/LICENSE-2.0
2141
+ *
2142
+ * Unless required by applicable law or agreed to in writing, software
2143
+ * distributed under the License is distributed on an "AS IS" BASIS,
2144
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2145
+ * See the License for the specific language governing permissions and
2146
+ * limitations under the License.
2147
+ */
2148
+ // This API is put in a separate file, so we can stub fetchConfig and activate in tests.
2149
+ // It's not possible to stub standalone functions from the same module.
2150
+ /**
2151
+ *
2152
+ * Performs fetch and activate operations, as a convenience.
2153
+ *
2154
+ * @param remoteConfig - The {@link RemoteConfig} instance.
2155
+ *
2156
+ * @returns A `Promise` which resolves to true if the current call activated the fetched configs.
2157
+ * If the fetched configs were already activated, the `Promise` will resolve to false.
2158
+ *
2159
+ * @public
2160
+ */
2161
+ async function fetchAndActivate(remoteConfig) {
2162
+ remoteConfig = getModularInstance(remoteConfig);
2163
+ await fetchConfig(remoteConfig);
2164
+ return activate(remoteConfig);
2165
+ }
2166
+ /**
2167
+ * This method provides two different checks:
2168
+ *
2169
+ * 1. Check if IndexedDB exists in the browser environment.
2170
+ * 2. Check if the current browser context allows IndexedDB `open()` calls.
2171
+ *
2172
+ * @returns A `Promise` which resolves to true if a {@link RemoteConfig} instance
2173
+ * can be initialized in this environment, or false if it cannot.
2174
+ * @public
2175
+ */
2176
+ async function isSupported() {
2177
+ if (!isIndexedDBAvailable()) {
2178
+ return false;
2179
+ }
2180
+ try {
2181
+ const isDBOpenable = await validateIndexedDBOpenable();
2182
+ return isDBOpenable;
2183
+ }
2184
+ catch (error) {
2185
+ return false;
2186
+ }
2187
+ }
2188
+
2189
+ /**
2190
+ * The Firebase Remote Config Web SDK.
2191
+ * This SDK does not work in a Node.js environment.
2192
+ *
2193
+ * @packageDocumentation
2194
+ */
2195
+ /** register component and version */
2196
+ registerRemoteConfig();
2197
+
2198
+ export { activate, ensureInitialized, fetchAndActivate, fetchConfig, getAll, getBoolean, getNumber, getRemoteConfig, getString, getValue, isSupported, onConfigUpdate, setCustomSignals, setLogLevel };
2199
+ //# sourceMappingURL=index.esm.js.map