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