@firebase/remote-config 0.6.6 → 0.7.0-canary.cb3bdd812
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.
- package/dist/esm/index.esm.js +737 -8
- package/dist/esm/index.esm.js.map +1 -1
- package/dist/esm/src/api.d.ts +17 -1
- package/dist/esm/src/client/eventEmitter.d.ts +39 -0
- package/dist/esm/src/client/realtime_handler.d.ts +141 -0
- package/dist/esm/src/client/remote_config_fetch_client.d.ts +13 -1
- package/dist/esm/src/client/visibility_monitor.d.ts +23 -0
- package/dist/esm/src/errors.d.ts +17 -1
- package/dist/esm/src/public_types.d.ts +60 -1
- package/dist/esm/src/remote_config.d.ts +10 -1
- package/dist/esm/src/storage/storage.d.ts +9 -1
- package/dist/index.cjs.js +736 -6
- package/dist/index.cjs.js.map +1 -1
- package/dist/remote-config-public.d.ts +81 -0
- package/dist/remote-config.d.ts +81 -0
- package/dist/src/api.d.ts +17 -1
- package/dist/src/client/eventEmitter.d.ts +39 -0
- package/dist/src/client/realtime_handler.d.ts +141 -0
- package/dist/src/client/remote_config_fetch_client.d.ts +13 -1
- package/dist/src/client/visibility_monitor.d.ts +23 -0
- package/dist/src/errors.d.ts +17 -1
- package/dist/src/global_index.d.ts +90 -1
- package/dist/src/public_types.d.ts +60 -1
- package/dist/src/remote_config.d.ts +10 -1
- package/dist/src/storage/storage.d.ts +9 -1
- package/package.json +7 -7
package/dist/esm/index.esm.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { _getProvider, getApp, _registerComponent, registerVersion, SDK_VERSION } from '@firebase/app';
|
|
2
|
-
import { ErrorFactory, FirebaseError, getModularInstance, deepEqual, calculateBackoffMillis, isIndexedDBAvailable, validateIndexedDBOpenable } from '@firebase/util';
|
|
2
|
+
import { ErrorFactory, FirebaseError, getModularInstance, deepEqual, calculateBackoffMillis, assert, isIndexedDBAvailable, validateIndexedDBOpenable } from '@firebase/util';
|
|
3
3
|
import { Component } from '@firebase/component';
|
|
4
4
|
import { LogLevel, Logger } from '@firebase/logger';
|
|
5
5
|
import '@firebase/installations';
|
|
6
6
|
|
|
7
7
|
const name = "@firebase/remote-config";
|
|
8
|
-
const version = "0.
|
|
8
|
+
const version = "0.7.0-canary.cb3bdd812";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @license
|
|
@@ -101,7 +101,11 @@ const ERROR_DESCRIPTION_MAP = {
|
|
|
101
101
|
' Original error: {$originalErrorMessage}.',
|
|
102
102
|
["fetch-status" /* ErrorCode.FETCH_STATUS */]: 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.',
|
|
103
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.'
|
|
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}'
|
|
105
109
|
};
|
|
106
110
|
const ERROR_FACTORY = new ErrorFactory('remoteconfig' /* service */, 'Remote Config' /* service name */, ERROR_DESCRIPTION_MAP);
|
|
107
111
|
// Note how this is like typeof/instanceof, but for ErrorCode.
|
|
@@ -201,6 +205,7 @@ function getRemoteConfig(app = getApp(), options = {}) {
|
|
|
201
205
|
rc._initializePromise = Promise.all([
|
|
202
206
|
rc._storage.setLastSuccessfulFetchResponse(options.initialFetchResponse),
|
|
203
207
|
rc._storage.setActiveConfigEtag(options.initialFetchResponse?.eTag || ''),
|
|
208
|
+
rc._storage.setActiveConfigTemplateVersion(options.initialFetchResponse.templateVersion || 0),
|
|
204
209
|
rc._storageCache.setLastSuccessfulFetchTimestampMillis(Date.now()),
|
|
205
210
|
rc._storageCache.setLastFetchStatus('success'),
|
|
206
211
|
rc._storageCache.setActiveConfig(options.initialFetchResponse?.config || {})
|
|
@@ -228,6 +233,7 @@ async function activate(remoteConfig) {
|
|
|
228
233
|
if (!lastSuccessfulFetchResponse ||
|
|
229
234
|
!lastSuccessfulFetchResponse.config ||
|
|
230
235
|
!lastSuccessfulFetchResponse.eTag ||
|
|
236
|
+
!lastSuccessfulFetchResponse.templateVersion ||
|
|
231
237
|
lastSuccessfulFetchResponse.eTag === activeConfigEtag) {
|
|
232
238
|
// Either there is no successful fetched config, or is the same as current active
|
|
233
239
|
// config.
|
|
@@ -235,7 +241,8 @@ async function activate(remoteConfig) {
|
|
|
235
241
|
}
|
|
236
242
|
await Promise.all([
|
|
237
243
|
rc._storageCache.setActiveConfig(lastSuccessfulFetchResponse.config),
|
|
238
|
-
rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag)
|
|
244
|
+
rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag),
|
|
245
|
+
rc._storage.setActiveConfigTemplateVersion(lastSuccessfulFetchResponse.templateVersion)
|
|
239
246
|
]);
|
|
240
247
|
return true;
|
|
241
248
|
}
|
|
@@ -445,6 +452,29 @@ async function setCustomSignals(remoteConfig, customSignals) {
|
|
|
445
452
|
rc._logger.error(`Error encountered while setting custom signals: ${error}`);
|
|
446
453
|
}
|
|
447
454
|
}
|
|
455
|
+
// TODO: Add public document for the Remote Config Realtime API guide on the Web Platform.
|
|
456
|
+
/**
|
|
457
|
+
* Starts listening for real-time config updates from the Remote Config backend and automatically
|
|
458
|
+
* fetches updates from the Remote Config backend when they are available.
|
|
459
|
+
*
|
|
460
|
+
* @remarks
|
|
461
|
+
* If a connection to the Remote Config backend is not already open, calling this method will
|
|
462
|
+
* open it. Multiple listeners can be added by calling this method again, but subsequent calls
|
|
463
|
+
* re-use the same connection to the backend.
|
|
464
|
+
*
|
|
465
|
+
* @param remoteConfig - The {@link RemoteConfig} instance.
|
|
466
|
+
* @param observer - The {@link ConfigUpdateObserver} to be notified of config updates.
|
|
467
|
+
* @returns An {@link Unsubscribe} function to remove the listener.
|
|
468
|
+
*
|
|
469
|
+
* @public
|
|
470
|
+
*/
|
|
471
|
+
function onConfigUpdate(remoteConfig, observer) {
|
|
472
|
+
const rc = getModularInstance(remoteConfig);
|
|
473
|
+
rc._realtimeHandler.addObserver(observer);
|
|
474
|
+
return () => {
|
|
475
|
+
rc._realtimeHandler.removeObserver(observer);
|
|
476
|
+
};
|
|
477
|
+
}
|
|
448
478
|
|
|
449
479
|
/**
|
|
450
480
|
* @license
|
|
@@ -618,6 +648,8 @@ class RestClient {
|
|
|
618
648
|
// Deviates from pure decorator by not passing max-age header since we don't currently have
|
|
619
649
|
// service behavior using that header.
|
|
620
650
|
'If-None-Match': request.eTag || '*'
|
|
651
|
+
// TODO: Add this header once CORS error is fixed internally.
|
|
652
|
+
//'X-Firebase-RC-Fetch-Type': `${fetchType}/${fetchAttempt}`
|
|
621
653
|
};
|
|
622
654
|
const requestBody = {
|
|
623
655
|
/* eslint-disable camelcase */
|
|
@@ -664,6 +696,7 @@ class RestClient {
|
|
|
664
696
|
const responseEtag = response.headers.get('ETag') || undefined;
|
|
665
697
|
let config;
|
|
666
698
|
let state;
|
|
699
|
+
let templateVersion;
|
|
667
700
|
// JSON parsing throws SyntaxError if the response body isn't a JSON string.
|
|
668
701
|
// Requesting application/json and checking for a 200 ensures there's JSON data.
|
|
669
702
|
if (response.status === 200) {
|
|
@@ -678,6 +711,7 @@ class RestClient {
|
|
|
678
711
|
}
|
|
679
712
|
config = responseBody['entries'];
|
|
680
713
|
state = responseBody['state'];
|
|
714
|
+
templateVersion = responseBody['templateVersion'];
|
|
681
715
|
}
|
|
682
716
|
// Normalizes based on legacy state.
|
|
683
717
|
if (state === 'INSTANCE_STATE_UNSPECIFIED') {
|
|
@@ -699,7 +733,7 @@ class RestClient {
|
|
|
699
733
|
httpStatus: status
|
|
700
734
|
});
|
|
701
735
|
}
|
|
702
|
-
return { status, eTag: responseEtag, config };
|
|
736
|
+
return { status, eTag: responseEtag, config, templateVersion };
|
|
703
737
|
}
|
|
704
738
|
}
|
|
705
739
|
|
|
@@ -861,12 +895,17 @@ class RemoteConfig {
|
|
|
861
895
|
/**
|
|
862
896
|
* @internal
|
|
863
897
|
*/
|
|
864
|
-
_logger
|
|
898
|
+
_logger,
|
|
899
|
+
/**
|
|
900
|
+
* @internal
|
|
901
|
+
*/
|
|
902
|
+
_realtimeHandler) {
|
|
865
903
|
this.app = app;
|
|
866
904
|
this._client = _client;
|
|
867
905
|
this._storageCache = _storageCache;
|
|
868
906
|
this._storage = _storage;
|
|
869
907
|
this._logger = _logger;
|
|
908
|
+
this._realtimeHandler = _realtimeHandler;
|
|
870
909
|
/**
|
|
871
910
|
* Tracks completion of initialization promise.
|
|
872
911
|
* @internal
|
|
@@ -999,6 +1038,18 @@ class Storage {
|
|
|
999
1038
|
getCustomSignals() {
|
|
1000
1039
|
return this.get('custom_signals');
|
|
1001
1040
|
}
|
|
1041
|
+
getRealtimeBackoffMetadata() {
|
|
1042
|
+
return this.get('realtime_backoff_metadata');
|
|
1043
|
+
}
|
|
1044
|
+
setRealtimeBackoffMetadata(realtimeMetadata) {
|
|
1045
|
+
return this.set('realtime_backoff_metadata', realtimeMetadata);
|
|
1046
|
+
}
|
|
1047
|
+
getActiveConfigTemplateVersion() {
|
|
1048
|
+
return this.get('last_known_template_version');
|
|
1049
|
+
}
|
|
1050
|
+
setActiveConfigTemplateVersion(version) {
|
|
1051
|
+
return this.set('last_known_template_version', version);
|
|
1052
|
+
}
|
|
1002
1053
|
}
|
|
1003
1054
|
class IndexedDbStorage extends Storage {
|
|
1004
1055
|
/**
|
|
@@ -1259,6 +1310,683 @@ class StorageCache {
|
|
|
1259
1310
|
}
|
|
1260
1311
|
}
|
|
1261
1312
|
|
|
1313
|
+
/**
|
|
1314
|
+
* @license
|
|
1315
|
+
* Copyright 2025 Google LLC
|
|
1316
|
+
*
|
|
1317
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1318
|
+
* you may not use this file except in compliance with the License.
|
|
1319
|
+
* You may obtain a copy of the License at
|
|
1320
|
+
*
|
|
1321
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1322
|
+
*
|
|
1323
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1324
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1325
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1326
|
+
* See the License for the specific language governing permissions and
|
|
1327
|
+
* limitations under the License.
|
|
1328
|
+
*/
|
|
1329
|
+
// 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.
|
|
1330
|
+
/**
|
|
1331
|
+
* Base class to be used if you want to emit events. Call the constructor with
|
|
1332
|
+
* the set of allowed event names.
|
|
1333
|
+
*/
|
|
1334
|
+
class EventEmitter {
|
|
1335
|
+
constructor(allowedEvents_) {
|
|
1336
|
+
this.allowedEvents_ = allowedEvents_;
|
|
1337
|
+
this.listeners_ = {};
|
|
1338
|
+
assert(Array.isArray(allowedEvents_) && allowedEvents_.length > 0, 'Requires a non-empty array');
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* To be called by derived classes to trigger events.
|
|
1342
|
+
*/
|
|
1343
|
+
trigger(eventType, ...varArgs) {
|
|
1344
|
+
if (Array.isArray(this.listeners_[eventType])) {
|
|
1345
|
+
// Clone the list, since callbacks could add/remove listeners.
|
|
1346
|
+
const listeners = [...this.listeners_[eventType]];
|
|
1347
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
1348
|
+
listeners[i].callback.apply(listeners[i].context, varArgs);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
on(eventType, callback, context) {
|
|
1353
|
+
this.validateEventType_(eventType);
|
|
1354
|
+
this.listeners_[eventType] = this.listeners_[eventType] || [];
|
|
1355
|
+
this.listeners_[eventType].push({ callback, context });
|
|
1356
|
+
const eventData = this.getInitialEvent(eventType);
|
|
1357
|
+
if (eventData) {
|
|
1358
|
+
//@ts-ignore
|
|
1359
|
+
callback.apply(context, eventData);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
off(eventType, callback, context) {
|
|
1363
|
+
this.validateEventType_(eventType);
|
|
1364
|
+
const listeners = this.listeners_[eventType] || [];
|
|
1365
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
1366
|
+
if (listeners[i].callback === callback &&
|
|
1367
|
+
(!context || context === listeners[i].context)) {
|
|
1368
|
+
listeners.splice(i, 1);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
validateEventType_(eventType) {
|
|
1374
|
+
assert(this.allowedEvents_.find(et => {
|
|
1375
|
+
return et === eventType;
|
|
1376
|
+
}), 'Unknown event: ' + eventType);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* @license
|
|
1382
|
+
* Copyright 2025 Google LLC
|
|
1383
|
+
*
|
|
1384
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1385
|
+
* you may not use this file except in compliance with the License.
|
|
1386
|
+
* You may obtain a copy of the License at
|
|
1387
|
+
*
|
|
1388
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1389
|
+
*
|
|
1390
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1391
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1392
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1393
|
+
* See the License for the specific language governing permissions and
|
|
1394
|
+
* limitations under the License.
|
|
1395
|
+
*/
|
|
1396
|
+
// 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.
|
|
1397
|
+
class VisibilityMonitor extends EventEmitter {
|
|
1398
|
+
static getInstance() {
|
|
1399
|
+
return new VisibilityMonitor();
|
|
1400
|
+
}
|
|
1401
|
+
constructor() {
|
|
1402
|
+
super(['visible']);
|
|
1403
|
+
let hidden;
|
|
1404
|
+
let visibilityChange;
|
|
1405
|
+
if (typeof document !== 'undefined' &&
|
|
1406
|
+
typeof document.addEventListener !== 'undefined') {
|
|
1407
|
+
if (typeof document['hidden'] !== 'undefined') {
|
|
1408
|
+
// Opera 12.10 and Firefox 18 and later support
|
|
1409
|
+
visibilityChange = 'visibilitychange';
|
|
1410
|
+
hidden = 'hidden';
|
|
1411
|
+
} // @ts-ignore
|
|
1412
|
+
else if (typeof document['mozHidden'] !== 'undefined') {
|
|
1413
|
+
visibilityChange = 'mozvisibilitychange';
|
|
1414
|
+
hidden = 'mozHidden';
|
|
1415
|
+
} // @ts-ignore
|
|
1416
|
+
else if (typeof document['msHidden'] !== 'undefined') {
|
|
1417
|
+
visibilityChange = 'msvisibilitychange';
|
|
1418
|
+
hidden = 'msHidden';
|
|
1419
|
+
} // @ts-ignore
|
|
1420
|
+
else if (typeof document['webkitHidden'] !== 'undefined') {
|
|
1421
|
+
visibilityChange = 'webkitvisibilitychange';
|
|
1422
|
+
hidden = 'webkitHidden';
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
// Initially, we always assume we are visible. This ensures that in browsers
|
|
1426
|
+
// without page visibility support or in cases where we are never visible
|
|
1427
|
+
// (e.g. chrome extension), we act as if we are visible, i.e. don't delay
|
|
1428
|
+
// reconnects
|
|
1429
|
+
this.visible_ = true;
|
|
1430
|
+
// @ts-ignore
|
|
1431
|
+
if (visibilityChange) {
|
|
1432
|
+
document.addEventListener(visibilityChange, () => {
|
|
1433
|
+
// @ts-ignore
|
|
1434
|
+
const visible = !document[hidden];
|
|
1435
|
+
if (visible !== this.visible_) {
|
|
1436
|
+
this.visible_ = visible;
|
|
1437
|
+
this.trigger('visible', visible);
|
|
1438
|
+
}
|
|
1439
|
+
}, false);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
getInitialEvent(eventType) {
|
|
1443
|
+
assert(eventType === 'visible', 'Unknown event type: ' + eventType);
|
|
1444
|
+
return [this.visible_];
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* @license
|
|
1450
|
+
* Copyright 2025 Google LLC
|
|
1451
|
+
*
|
|
1452
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1453
|
+
* you may not use this file except in compliance with the License.
|
|
1454
|
+
* You may obtain a copy of the License at
|
|
1455
|
+
*
|
|
1456
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1457
|
+
*
|
|
1458
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1459
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1460
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1461
|
+
* See the License for the specific language governing permissions and
|
|
1462
|
+
* limitations under the License.
|
|
1463
|
+
*/
|
|
1464
|
+
const API_KEY_HEADER = 'X-Goog-Api-Key';
|
|
1465
|
+
const INSTALLATIONS_AUTH_TOKEN_HEADER = 'X-Goog-Firebase-Installations-Auth';
|
|
1466
|
+
const ORIGINAL_RETRIES = 8;
|
|
1467
|
+
const MAXIMUM_FETCH_ATTEMPTS = 3;
|
|
1468
|
+
const NO_BACKOFF_TIME_IN_MILLIS = -1;
|
|
1469
|
+
const NO_FAILED_REALTIME_STREAMS = 0;
|
|
1470
|
+
const REALTIME_DISABLED_KEY = 'featureDisabled';
|
|
1471
|
+
const REALTIME_RETRY_INTERVAL = 'retryIntervalSeconds';
|
|
1472
|
+
const TEMPLATE_VERSION_KEY = 'latestTemplateVersionNumber';
|
|
1473
|
+
class RealtimeHandler {
|
|
1474
|
+
constructor(firebaseInstallations, storage, sdkVersion, namespace, projectId, apiKey, appId, logger, storageCache, cachingClient) {
|
|
1475
|
+
this.firebaseInstallations = firebaseInstallations;
|
|
1476
|
+
this.storage = storage;
|
|
1477
|
+
this.sdkVersion = sdkVersion;
|
|
1478
|
+
this.namespace = namespace;
|
|
1479
|
+
this.projectId = projectId;
|
|
1480
|
+
this.apiKey = apiKey;
|
|
1481
|
+
this.appId = appId;
|
|
1482
|
+
this.logger = logger;
|
|
1483
|
+
this.storageCache = storageCache;
|
|
1484
|
+
this.cachingClient = cachingClient;
|
|
1485
|
+
this.observers = new Set();
|
|
1486
|
+
this.isConnectionActive = false;
|
|
1487
|
+
this.isRealtimeDisabled = false;
|
|
1488
|
+
this.httpRetriesRemaining = ORIGINAL_RETRIES;
|
|
1489
|
+
this.isInBackground = false;
|
|
1490
|
+
this.decoder = new TextDecoder('utf-8');
|
|
1491
|
+
this.isClosingConnection = false;
|
|
1492
|
+
this.propagateError = (e) => this.observers.forEach(o => o.error?.(e));
|
|
1493
|
+
/**
|
|
1494
|
+
* HTTP status code that the Realtime client should retry on.
|
|
1495
|
+
*/
|
|
1496
|
+
this.isStatusCodeRetryable = (statusCode) => {
|
|
1497
|
+
const retryableStatusCodes = [
|
|
1498
|
+
408, // Request Timeout
|
|
1499
|
+
429, // Too Many Requests
|
|
1500
|
+
502, // Bad Gateway
|
|
1501
|
+
503, // Service Unavailable
|
|
1502
|
+
504 // Gateway Timeout
|
|
1503
|
+
];
|
|
1504
|
+
return !statusCode || retryableStatusCodes.includes(statusCode);
|
|
1505
|
+
};
|
|
1506
|
+
void this.setRetriesRemaining();
|
|
1507
|
+
void VisibilityMonitor.getInstance().on('visible', this.onVisibilityChange, this);
|
|
1508
|
+
}
|
|
1509
|
+
async setRetriesRemaining() {
|
|
1510
|
+
// Retrieve number of remaining retries from last session. The minimum retry count being one.
|
|
1511
|
+
const metadata = await this.storage.getRealtimeBackoffMetadata();
|
|
1512
|
+
const numFailedStreams = metadata?.numFailedStreams || 0;
|
|
1513
|
+
this.httpRetriesRemaining = Math.max(ORIGINAL_RETRIES - numFailedStreams, 1);
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Increment the number of failed stream attempts, increase the backoff duration, set the backoff
|
|
1517
|
+
* end time to "backoff duration" after `lastFailedStreamTime` and persist the new
|
|
1518
|
+
* values to storage metadata.
|
|
1519
|
+
*/
|
|
1520
|
+
async updateBackoffMetadataWithLastFailedStreamConnectionTime(lastFailedStreamTime) {
|
|
1521
|
+
const numFailedStreams = ((await this.storage.getRealtimeBackoffMetadata())?.numFailedStreams ||
|
|
1522
|
+
0) + 1;
|
|
1523
|
+
const backoffMillis = calculateBackoffMillis(numFailedStreams, 60000, 2);
|
|
1524
|
+
await this.storage.setRealtimeBackoffMetadata({
|
|
1525
|
+
backoffEndTimeMillis: new Date(lastFailedStreamTime.getTime() + backoffMillis),
|
|
1526
|
+
numFailedStreams
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Increase the backoff duration with a new end time based on Retry Interval.
|
|
1531
|
+
*/
|
|
1532
|
+
async updateBackoffMetadataWithRetryInterval(retryIntervalSeconds) {
|
|
1533
|
+
const currentTime = Date.now();
|
|
1534
|
+
const backoffDurationInMillis = retryIntervalSeconds * 1000;
|
|
1535
|
+
const backoffEndTime = new Date(currentTime + backoffDurationInMillis);
|
|
1536
|
+
const numFailedStreams = 0;
|
|
1537
|
+
await this.storage.setRealtimeBackoffMetadata({
|
|
1538
|
+
backoffEndTimeMillis: backoffEndTime,
|
|
1539
|
+
numFailedStreams
|
|
1540
|
+
});
|
|
1541
|
+
await this.retryHttpConnectionWhenBackoffEnds();
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Closes the realtime HTTP connection.
|
|
1545
|
+
* Note: This method is designed to be called only once at a time.
|
|
1546
|
+
* If a call is already in progress, subsequent calls will be ignored.
|
|
1547
|
+
*/
|
|
1548
|
+
async closeRealtimeHttpConnection() {
|
|
1549
|
+
if (this.isClosingConnection) {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
this.isClosingConnection = true;
|
|
1553
|
+
try {
|
|
1554
|
+
if (this.reader) {
|
|
1555
|
+
await this.reader.cancel();
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
catch (e) {
|
|
1559
|
+
// The network connection was lost, so cancel() failed.
|
|
1560
|
+
// This is expected in a disconnected state, so we can safely ignore the error.
|
|
1561
|
+
this.logger.debug('Failed to cancel the reader, connection was lost.');
|
|
1562
|
+
}
|
|
1563
|
+
finally {
|
|
1564
|
+
this.reader = undefined;
|
|
1565
|
+
}
|
|
1566
|
+
if (this.controller) {
|
|
1567
|
+
await this.controller.abort();
|
|
1568
|
+
this.controller = undefined;
|
|
1569
|
+
}
|
|
1570
|
+
this.isClosingConnection = false;
|
|
1571
|
+
}
|
|
1572
|
+
async resetRealtimeBackoff() {
|
|
1573
|
+
await this.storage.setRealtimeBackoffMetadata({
|
|
1574
|
+
backoffEndTimeMillis: new Date(-1),
|
|
1575
|
+
numFailedStreams: 0
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
resetRetryCount() {
|
|
1579
|
+
this.httpRetriesRemaining = ORIGINAL_RETRIES;
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Assembles the request headers and body and executes the fetch request to
|
|
1583
|
+
* establish the real-time streaming connection. This is the "worker" method
|
|
1584
|
+
* that performs the actual network communication.
|
|
1585
|
+
*/
|
|
1586
|
+
async establishRealtimeConnection(url, installationId, installationTokenResult, signal) {
|
|
1587
|
+
const eTagValue = await this.storage.getActiveConfigEtag();
|
|
1588
|
+
const lastKnownVersionNumber = await this.storage.getActiveConfigTemplateVersion();
|
|
1589
|
+
const headers = {
|
|
1590
|
+
[API_KEY_HEADER]: this.apiKey,
|
|
1591
|
+
[INSTALLATIONS_AUTH_TOKEN_HEADER]: installationTokenResult,
|
|
1592
|
+
'Content-Type': 'application/json',
|
|
1593
|
+
'Accept': 'application/json',
|
|
1594
|
+
'If-None-Match': eTagValue || '*',
|
|
1595
|
+
'Content-Encoding': 'gzip'
|
|
1596
|
+
};
|
|
1597
|
+
const requestBody = {
|
|
1598
|
+
project: this.projectId,
|
|
1599
|
+
namespace: this.namespace,
|
|
1600
|
+
lastKnownVersionNumber,
|
|
1601
|
+
appId: this.appId,
|
|
1602
|
+
sdkVersion: this.sdkVersion,
|
|
1603
|
+
appInstanceId: installationId
|
|
1604
|
+
};
|
|
1605
|
+
const response = await fetch(url, {
|
|
1606
|
+
method: 'POST',
|
|
1607
|
+
headers,
|
|
1608
|
+
body: JSON.stringify(requestBody),
|
|
1609
|
+
signal
|
|
1610
|
+
});
|
|
1611
|
+
return response;
|
|
1612
|
+
}
|
|
1613
|
+
getRealtimeUrl() {
|
|
1614
|
+
const urlBase = window.FIREBASE_REMOTE_CONFIG_URL_BASE ||
|
|
1615
|
+
'https://firebaseremoteconfigrealtime.googleapis.com';
|
|
1616
|
+
const urlString = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:streamFetchInvalidations?key=${this.apiKey}`;
|
|
1617
|
+
return new URL(urlString);
|
|
1618
|
+
}
|
|
1619
|
+
async createRealtimeConnection() {
|
|
1620
|
+
const [installationId, installationTokenResult] = await Promise.all([
|
|
1621
|
+
this.firebaseInstallations.getId(),
|
|
1622
|
+
this.firebaseInstallations.getToken(false)
|
|
1623
|
+
]);
|
|
1624
|
+
this.controller = new AbortController();
|
|
1625
|
+
const url = this.getRealtimeUrl();
|
|
1626
|
+
const realtimeConnection = await this.establishRealtimeConnection(url, installationId, installationTokenResult, this.controller.signal);
|
|
1627
|
+
return realtimeConnection;
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Retries HTTP stream connection asyncly in random time intervals.
|
|
1631
|
+
*/
|
|
1632
|
+
async retryHttpConnectionWhenBackoffEnds() {
|
|
1633
|
+
let backoffMetadata = await this.storage.getRealtimeBackoffMetadata();
|
|
1634
|
+
if (!backoffMetadata) {
|
|
1635
|
+
backoffMetadata = {
|
|
1636
|
+
backoffEndTimeMillis: new Date(NO_BACKOFF_TIME_IN_MILLIS),
|
|
1637
|
+
numFailedStreams: NO_FAILED_REALTIME_STREAMS
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
const backoffEndTime = new Date(backoffMetadata.backoffEndTimeMillis).getTime();
|
|
1641
|
+
const currentTime = Date.now();
|
|
1642
|
+
const retryMillis = Math.max(0, backoffEndTime - currentTime);
|
|
1643
|
+
await this.makeRealtimeHttpConnection(retryMillis);
|
|
1644
|
+
}
|
|
1645
|
+
setIsHttpConnectionRunning(connectionRunning) {
|
|
1646
|
+
this.isConnectionActive = connectionRunning;
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Combines the check and set operations to prevent multiple asynchronous
|
|
1650
|
+
* calls from redundantly starting an HTTP connection. This ensures that
|
|
1651
|
+
* only one attempt is made at a time.
|
|
1652
|
+
*/
|
|
1653
|
+
checkAndSetHttpConnectionFlagIfNotRunning() {
|
|
1654
|
+
const canMakeConnection = this.canEstablishStreamConnection();
|
|
1655
|
+
if (canMakeConnection) {
|
|
1656
|
+
this.setIsHttpConnectionRunning(true);
|
|
1657
|
+
}
|
|
1658
|
+
return canMakeConnection;
|
|
1659
|
+
}
|
|
1660
|
+
fetchResponseIsUpToDate(fetchResponse, lastKnownVersion) {
|
|
1661
|
+
// If there is a config, make sure its version is >= the last known version.
|
|
1662
|
+
if (fetchResponse.config != null && fetchResponse.templateVersion) {
|
|
1663
|
+
return fetchResponse.templateVersion >= lastKnownVersion;
|
|
1664
|
+
}
|
|
1665
|
+
// If there isn't a config, return true if the fetch was successful and backend had no update.
|
|
1666
|
+
// Else, it returned an out of date config.
|
|
1667
|
+
return this.storageCache.getLastFetchStatus() === 'success';
|
|
1668
|
+
}
|
|
1669
|
+
parseAndValidateConfigUpdateMessage(message) {
|
|
1670
|
+
const left = message.indexOf('{');
|
|
1671
|
+
const right = message.indexOf('}', left);
|
|
1672
|
+
if (left < 0 || right < 0) {
|
|
1673
|
+
return '';
|
|
1674
|
+
}
|
|
1675
|
+
return left >= right ? '' : message.substring(left, right + 1);
|
|
1676
|
+
}
|
|
1677
|
+
isEventListenersEmpty() {
|
|
1678
|
+
return this.observers.size === 0;
|
|
1679
|
+
}
|
|
1680
|
+
getRandomInt(max) {
|
|
1681
|
+
return Math.floor(Math.random() * max);
|
|
1682
|
+
}
|
|
1683
|
+
executeAllListenerCallbacks(configUpdate) {
|
|
1684
|
+
this.observers.forEach(observer => observer.next(configUpdate));
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Compares two configuration objects and returns a set of keys that have changed.
|
|
1688
|
+
* A key is considered changed if it's new, removed, or has a different value.
|
|
1689
|
+
*/
|
|
1690
|
+
getChangedParams(newConfig, oldConfig) {
|
|
1691
|
+
const changedKeys = new Set();
|
|
1692
|
+
const newKeys = new Set(Object.keys(newConfig || {}));
|
|
1693
|
+
const oldKeys = new Set(Object.keys(oldConfig || {}));
|
|
1694
|
+
for (const key of newKeys) {
|
|
1695
|
+
if (!oldKeys.has(key) || newConfig[key] !== oldConfig[key]) {
|
|
1696
|
+
changedKeys.add(key);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
for (const key of oldKeys) {
|
|
1700
|
+
if (!newKeys.has(key)) {
|
|
1701
|
+
changedKeys.add(key);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return changedKeys;
|
|
1705
|
+
}
|
|
1706
|
+
async fetchLatestConfig(remainingAttempts, targetVersion) {
|
|
1707
|
+
const remainingAttemptsAfterFetch = remainingAttempts - 1;
|
|
1708
|
+
const currentAttempt = MAXIMUM_FETCH_ATTEMPTS - remainingAttemptsAfterFetch;
|
|
1709
|
+
const customSignals = this.storageCache.getCustomSignals();
|
|
1710
|
+
if (customSignals) {
|
|
1711
|
+
this.logger.debug(`Fetching config with custom signals: ${JSON.stringify(customSignals)}`);
|
|
1712
|
+
}
|
|
1713
|
+
const abortSignal = new RemoteConfigAbortSignal();
|
|
1714
|
+
try {
|
|
1715
|
+
const fetchRequest = {
|
|
1716
|
+
cacheMaxAgeMillis: 0,
|
|
1717
|
+
signal: abortSignal,
|
|
1718
|
+
customSignals,
|
|
1719
|
+
fetchType: 'REALTIME',
|
|
1720
|
+
fetchAttempt: currentAttempt
|
|
1721
|
+
};
|
|
1722
|
+
const fetchResponse = await this.cachingClient.fetch(fetchRequest);
|
|
1723
|
+
let activatedConfigs = await this.storage.getActiveConfig();
|
|
1724
|
+
if (!this.fetchResponseIsUpToDate(fetchResponse, targetVersion)) {
|
|
1725
|
+
this.logger.debug("Fetched template version is the same as SDK's current version." +
|
|
1726
|
+
' Retrying fetch.');
|
|
1727
|
+
// Continue fetching until template version number is greater than current.
|
|
1728
|
+
await this.autoFetch(remainingAttemptsAfterFetch, targetVersion);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (fetchResponse.config == null) {
|
|
1732
|
+
this.logger.debug('The fetch succeeded, but the backend had no updates.');
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (activatedConfigs == null) {
|
|
1736
|
+
activatedConfigs = {};
|
|
1737
|
+
}
|
|
1738
|
+
const updatedKeys = this.getChangedParams(fetchResponse.config, activatedConfigs);
|
|
1739
|
+
if (updatedKeys.size === 0) {
|
|
1740
|
+
this.logger.debug('Config was fetched, but no params changed.');
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
const configUpdate = {
|
|
1744
|
+
getUpdatedKeys() {
|
|
1745
|
+
return new Set(updatedKeys);
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
this.executeAllListenerCallbacks(configUpdate);
|
|
1749
|
+
}
|
|
1750
|
+
catch (e) {
|
|
1751
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1752
|
+
const error = ERROR_FACTORY.create("update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */, {
|
|
1753
|
+
originalErrorMessage: `Failed to auto-fetch config update: ${errorMessage}`
|
|
1754
|
+
});
|
|
1755
|
+
this.propagateError(error);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async autoFetch(remainingAttempts, targetVersion) {
|
|
1759
|
+
if (remainingAttempts === 0) {
|
|
1760
|
+
const error = ERROR_FACTORY.create("update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */, {
|
|
1761
|
+
originalErrorMessage: 'Unable to fetch the latest version of the template.'
|
|
1762
|
+
});
|
|
1763
|
+
this.propagateError(error);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const timeTillFetchSeconds = this.getRandomInt(4);
|
|
1767
|
+
const timeTillFetchInMiliseconds = timeTillFetchSeconds * 1000;
|
|
1768
|
+
await new Promise(resolve => setTimeout(resolve, timeTillFetchInMiliseconds));
|
|
1769
|
+
await this.fetchLatestConfig(remainingAttempts, targetVersion);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Processes a stream of real-time messages for configuration updates.
|
|
1773
|
+
* This method reassembles fragmented messages, validates and parses the JSON,
|
|
1774
|
+
* and automatically fetches a new config if a newer template version is available.
|
|
1775
|
+
* It also handles server-specified retry intervals and propagates errors for
|
|
1776
|
+
* invalid messages or when real-time updates are disabled.
|
|
1777
|
+
*/
|
|
1778
|
+
async handleNotifications(reader) {
|
|
1779
|
+
let partialConfigUpdateMessage;
|
|
1780
|
+
let currentConfigUpdateMessage = '';
|
|
1781
|
+
while (true) {
|
|
1782
|
+
const { done, value } = await reader.read();
|
|
1783
|
+
if (done) {
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
partialConfigUpdateMessage = this.decoder.decode(value, { stream: true });
|
|
1787
|
+
currentConfigUpdateMessage += partialConfigUpdateMessage;
|
|
1788
|
+
if (partialConfigUpdateMessage.includes('}')) {
|
|
1789
|
+
currentConfigUpdateMessage = this.parseAndValidateConfigUpdateMessage(currentConfigUpdateMessage);
|
|
1790
|
+
if (currentConfigUpdateMessage.length === 0) {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
try {
|
|
1794
|
+
const jsonObject = JSON.parse(currentConfigUpdateMessage);
|
|
1795
|
+
if (this.isEventListenersEmpty()) {
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
if (REALTIME_DISABLED_KEY in jsonObject &&
|
|
1799
|
+
jsonObject[REALTIME_DISABLED_KEY] === true) {
|
|
1800
|
+
const error = ERROR_FACTORY.create("realtime-unavailable" /* ErrorCode.CONFIG_UPDATE_UNAVAILABLE */, {
|
|
1801
|
+
originalErrorMessage: 'The server is temporarily unavailable. Try again in a few minutes.'
|
|
1802
|
+
});
|
|
1803
|
+
this.propagateError(error);
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
if (TEMPLATE_VERSION_KEY in jsonObject) {
|
|
1807
|
+
const oldTemplateVersion = await this.storage.getActiveConfigTemplateVersion();
|
|
1808
|
+
const targetTemplateVersion = Number(jsonObject[TEMPLATE_VERSION_KEY]);
|
|
1809
|
+
if (oldTemplateVersion &&
|
|
1810
|
+
targetTemplateVersion > oldTemplateVersion) {
|
|
1811
|
+
await this.autoFetch(MAXIMUM_FETCH_ATTEMPTS, targetTemplateVersion);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
// This field in the response indicates that the realtime request should retry after the
|
|
1815
|
+
// specified interval to establish a long-lived connection. This interval extends the
|
|
1816
|
+
// backoff duration without affecting the number of retries, so it will not enter an
|
|
1817
|
+
// exponential backoff state.
|
|
1818
|
+
if (REALTIME_RETRY_INTERVAL in jsonObject) {
|
|
1819
|
+
const retryIntervalSeconds = Number(jsonObject[REALTIME_RETRY_INTERVAL]);
|
|
1820
|
+
await this.updateBackoffMetadataWithRetryInterval(retryIntervalSeconds);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
catch (e) {
|
|
1824
|
+
this.logger.debug('Unable to parse latest config update message.', e);
|
|
1825
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1826
|
+
this.propagateError(ERROR_FACTORY.create("update-message-invalid" /* ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID */, {
|
|
1827
|
+
originalErrorMessage: errorMessage
|
|
1828
|
+
}));
|
|
1829
|
+
}
|
|
1830
|
+
currentConfigUpdateMessage = '';
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
async listenForNotifications(reader) {
|
|
1835
|
+
try {
|
|
1836
|
+
await this.handleNotifications(reader);
|
|
1837
|
+
}
|
|
1838
|
+
catch (e) {
|
|
1839
|
+
// If the real-time connection is at an unexpected lifecycle state when the app is
|
|
1840
|
+
// backgrounded, it's expected closing the connection will throw an exception.
|
|
1841
|
+
if (!this.isInBackground) {
|
|
1842
|
+
// Otherwise, the real-time server connection was closed due to a transient issue.
|
|
1843
|
+
this.logger.debug('Real-time connection was closed due to an exception.');
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Open the real-time connection, begin listening for updates, and auto-fetch when an update is
|
|
1849
|
+
* received.
|
|
1850
|
+
*
|
|
1851
|
+
* If the connection is successful, this method will block on its thread while it reads the
|
|
1852
|
+
* chunk-encoded HTTP body. When the connection closes, it attempts to reestablish the stream.
|
|
1853
|
+
*/
|
|
1854
|
+
async prepareAndBeginRealtimeHttpStream() {
|
|
1855
|
+
if (!this.checkAndSetHttpConnectionFlagIfNotRunning()) {
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
let backoffMetadata = await this.storage.getRealtimeBackoffMetadata();
|
|
1859
|
+
if (!backoffMetadata) {
|
|
1860
|
+
backoffMetadata = {
|
|
1861
|
+
backoffEndTimeMillis: new Date(NO_BACKOFF_TIME_IN_MILLIS),
|
|
1862
|
+
numFailedStreams: NO_FAILED_REALTIME_STREAMS
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
const backoffEndTime = backoffMetadata.backoffEndTimeMillis.getTime();
|
|
1866
|
+
if (Date.now() < backoffEndTime) {
|
|
1867
|
+
await this.retryHttpConnectionWhenBackoffEnds();
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
let response;
|
|
1871
|
+
let responseCode;
|
|
1872
|
+
try {
|
|
1873
|
+
response = await this.createRealtimeConnection();
|
|
1874
|
+
responseCode = response.status;
|
|
1875
|
+
if (response.ok && response.body) {
|
|
1876
|
+
this.resetRetryCount();
|
|
1877
|
+
await this.resetRealtimeBackoff();
|
|
1878
|
+
const reader = response.body.getReader();
|
|
1879
|
+
this.reader = reader;
|
|
1880
|
+
// Start listening for realtime notifications.
|
|
1881
|
+
await this.listenForNotifications(reader);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
catch (error) {
|
|
1885
|
+
if (this.isInBackground) {
|
|
1886
|
+
// It's possible the app was backgrounded while the connection was open, which
|
|
1887
|
+
// threw an exception trying to read the response. No real error here, so treat
|
|
1888
|
+
// this as a success, even if we haven't read a 200 response code yet.
|
|
1889
|
+
this.resetRetryCount();
|
|
1890
|
+
}
|
|
1891
|
+
else {
|
|
1892
|
+
//there might have been a transient error so the client will retry the connection.
|
|
1893
|
+
this.logger.debug('Exception connecting to real-time RC backend. Retrying the connection...:', error);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
finally {
|
|
1897
|
+
// Close HTTP connection and associated streams.
|
|
1898
|
+
await this.closeRealtimeHttpConnection();
|
|
1899
|
+
this.setIsHttpConnectionRunning(false);
|
|
1900
|
+
// Update backoff metadata if the connection failed in the foreground.
|
|
1901
|
+
const connectionFailed = !this.isInBackground &&
|
|
1902
|
+
(responseCode === undefined ||
|
|
1903
|
+
this.isStatusCodeRetryable(responseCode));
|
|
1904
|
+
if (connectionFailed) {
|
|
1905
|
+
await this.updateBackoffMetadataWithLastFailedStreamConnectionTime(new Date());
|
|
1906
|
+
}
|
|
1907
|
+
// If responseCode is null then no connection was made to server and the SDK should still retry.
|
|
1908
|
+
if (connectionFailed || response?.ok) {
|
|
1909
|
+
await this.retryHttpConnectionWhenBackoffEnds();
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
const errorMessage = `Unable to connect to the server. HTTP status code: ${responseCode}`;
|
|
1913
|
+
const firebaseError = ERROR_FACTORY.create("stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */, {
|
|
1914
|
+
originalErrorMessage: errorMessage
|
|
1915
|
+
});
|
|
1916
|
+
this.propagateError(firebaseError);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Checks whether connection can be made or not based on some conditions
|
|
1922
|
+
* @returns booelean
|
|
1923
|
+
*/
|
|
1924
|
+
canEstablishStreamConnection() {
|
|
1925
|
+
const hasActiveListeners = this.observers.size > 0;
|
|
1926
|
+
const isNotDisabled = !this.isRealtimeDisabled;
|
|
1927
|
+
const isNoConnectionActive = !this.isConnectionActive;
|
|
1928
|
+
const inForeground = !this.isInBackground;
|
|
1929
|
+
return (hasActiveListeners &&
|
|
1930
|
+
isNotDisabled &&
|
|
1931
|
+
isNoConnectionActive &&
|
|
1932
|
+
inForeground);
|
|
1933
|
+
}
|
|
1934
|
+
async makeRealtimeHttpConnection(delayMillis) {
|
|
1935
|
+
if (!this.canEstablishStreamConnection()) {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
if (this.httpRetriesRemaining > 0) {
|
|
1939
|
+
this.httpRetriesRemaining--;
|
|
1940
|
+
await new Promise(resolve => setTimeout(resolve, delayMillis));
|
|
1941
|
+
void this.prepareAndBeginRealtimeHttpStream();
|
|
1942
|
+
}
|
|
1943
|
+
else if (!this.isInBackground) {
|
|
1944
|
+
const error = ERROR_FACTORY.create("stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */, {
|
|
1945
|
+
originalErrorMessage: 'Unable to connect to the server. Check your connection and try again.'
|
|
1946
|
+
});
|
|
1947
|
+
this.propagateError(error);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
async beginRealtime() {
|
|
1951
|
+
if (this.observers.size > 0) {
|
|
1952
|
+
await this.makeRealtimeHttpConnection(0);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Adds an observer to the realtime updates.
|
|
1957
|
+
* @param observer The observer to add.
|
|
1958
|
+
*/
|
|
1959
|
+
addObserver(observer) {
|
|
1960
|
+
this.observers.add(observer);
|
|
1961
|
+
void this.beginRealtime();
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Removes an observer from the realtime updates.
|
|
1965
|
+
* @param observer The observer to remove.
|
|
1966
|
+
*/
|
|
1967
|
+
removeObserver(observer) {
|
|
1968
|
+
if (this.observers.has(observer)) {
|
|
1969
|
+
this.observers.delete(observer);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Handles changes to the application's visibility state, managing the real-time connection.
|
|
1974
|
+
*
|
|
1975
|
+
* When the application is moved to the background, this method closes the existing
|
|
1976
|
+
* real-time connection to save resources. When the application returns to the
|
|
1977
|
+
* foreground, it attempts to re-establish the connection.
|
|
1978
|
+
*/
|
|
1979
|
+
async onVisibilityChange(visible) {
|
|
1980
|
+
this.isInBackground = !visible;
|
|
1981
|
+
if (!visible) {
|
|
1982
|
+
await this.closeRealtimeHttpConnection();
|
|
1983
|
+
}
|
|
1984
|
+
else if (visible) {
|
|
1985
|
+
await this.beginRealtime();
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1262
1990
|
/**
|
|
1263
1991
|
* @license
|
|
1264
1992
|
* Copyright 2020 Google LLC
|
|
@@ -1313,7 +2041,8 @@ function registerRemoteConfig() {
|
|
|
1313
2041
|
SDK_VERSION, namespace, projectId, apiKey, appId);
|
|
1314
2042
|
const retryingClient = new RetryingClient(restClient, storage);
|
|
1315
2043
|
const cachingClient = new CachingClient(retryingClient, storage, storageCache, logger);
|
|
1316
|
-
const
|
|
2044
|
+
const realtimeHandler = new RealtimeHandler(installations, storage, SDK_VERSION, namespace, projectId, apiKey, appId, logger, storageCache, cachingClient);
|
|
2045
|
+
const remoteConfigInstance = new RemoteConfig(app, cachingClient, storageCache, storage, logger, realtimeHandler);
|
|
1317
2046
|
// Starts warming cache.
|
|
1318
2047
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1319
2048
|
ensureInitialized(remoteConfigInstance);
|
|
@@ -1387,5 +2116,5 @@ async function isSupported() {
|
|
|
1387
2116
|
/** register component and version */
|
|
1388
2117
|
registerRemoteConfig();
|
|
1389
2118
|
|
|
1390
|
-
export { activate, ensureInitialized, fetchAndActivate, fetchConfig, getAll, getBoolean, getNumber, getRemoteConfig, getString, getValue, isSupported, setCustomSignals, setLogLevel };
|
|
2119
|
+
export { activate, ensureInitialized, fetchAndActivate, fetchConfig, getAll, getBoolean, getNumber, getRemoteConfig, getString, getValue, isSupported, onConfigUpdate, setCustomSignals, setLogLevel };
|
|
1391
2120
|
//# sourceMappingURL=index.esm.js.map
|