@launchdarkly/js-client-sdk-common 1.21.0 → 1.22.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 (78) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cjs/LDClientImpl.d.ts.map +1 -1
  3. package/dist/cjs/api/LDOptions.d.ts +8 -0
  4. package/dist/cjs/api/LDOptions.d.ts.map +1 -1
  5. package/dist/cjs/api/datasource/ModeResolution.d.ts +75 -0
  6. package/dist/cjs/api/datasource/ModeResolution.d.ts.map +1 -0
  7. package/dist/cjs/api/datasource/index.d.ts +1 -0
  8. package/dist/cjs/api/datasource/index.d.ts.map +1 -1
  9. package/dist/cjs/configuration/Configuration.d.ts +6 -0
  10. package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
  11. package/dist/cjs/configuration/validateOptions.d.ts +1 -1
  12. package/dist/cjs/configuration/validateOptions.d.ts.map +1 -1
  13. package/dist/cjs/configuration/validators.d.ts +5 -2
  14. package/dist/cjs/configuration/validators.d.ts.map +1 -1
  15. package/dist/cjs/datasource/ModeResolver.d.ts +30 -0
  16. package/dist/cjs/datasource/ModeResolver.d.ts.map +1 -0
  17. package/dist/cjs/datasource/fdv2/CacheInitializer.d.ts +17 -0
  18. package/dist/cjs/datasource/fdv2/CacheInitializer.d.ts.map +1 -0
  19. package/dist/cjs/datasource/fdv2/FDv1PollingSynchronizer.d.ts +2 -0
  20. package/dist/cjs/datasource/fdv2/FDv1PollingSynchronizer.d.ts.map +1 -0
  21. package/dist/cjs/datasource/fdv2/FDv2SourceResult.d.ts +3 -1
  22. package/dist/cjs/datasource/fdv2/FDv2SourceResult.d.ts.map +1 -1
  23. package/dist/cjs/datasource/fdv2/calculatePollDelay.d.ts +2 -0
  24. package/dist/cjs/datasource/fdv2/calculatePollDelay.d.ts.map +1 -0
  25. package/dist/cjs/datasource/fdv2/index.d.ts +4 -0
  26. package/dist/cjs/datasource/fdv2/index.d.ts.map +1 -1
  27. package/dist/cjs/datasource/flagEvalMapper.d.ts +3 -3
  28. package/dist/cjs/flag-manager/FlagManager.d.ts +2 -1
  29. package/dist/cjs/flag-manager/FlagManager.d.ts.map +1 -1
  30. package/dist/cjs/flag-manager/FlagPersistence.d.ts +8 -1
  31. package/dist/cjs/flag-manager/FlagPersistence.d.ts.map +1 -1
  32. package/dist/cjs/index.cjs +336 -60
  33. package/dist/cjs/index.cjs.map +1 -1
  34. package/dist/cjs/index.d.ts +3 -1
  35. package/dist/cjs/index.d.ts.map +1 -1
  36. package/dist/cjs/storage/freshness.d.ts +27 -0
  37. package/dist/cjs/storage/freshness.d.ts.map +1 -0
  38. package/dist/cjs/storage/loadCachedFlags.d.ts +25 -0
  39. package/dist/cjs/storage/loadCachedFlags.d.ts.map +1 -0
  40. package/dist/esm/LDClientImpl.d.ts.map +1 -1
  41. package/dist/esm/api/LDOptions.d.ts +8 -0
  42. package/dist/esm/api/LDOptions.d.ts.map +1 -1
  43. package/dist/esm/api/datasource/ModeResolution.d.ts +75 -0
  44. package/dist/esm/api/datasource/ModeResolution.d.ts.map +1 -0
  45. package/dist/esm/api/datasource/index.d.ts +1 -0
  46. package/dist/esm/api/datasource/index.d.ts.map +1 -1
  47. package/dist/esm/configuration/Configuration.d.ts +6 -0
  48. package/dist/esm/configuration/Configuration.d.ts.map +1 -1
  49. package/dist/esm/configuration/validateOptions.d.ts +1 -1
  50. package/dist/esm/configuration/validateOptions.d.ts.map +1 -1
  51. package/dist/esm/configuration/validators.d.ts +5 -2
  52. package/dist/esm/configuration/validators.d.ts.map +1 -1
  53. package/dist/esm/datasource/ModeResolver.d.ts +30 -0
  54. package/dist/esm/datasource/ModeResolver.d.ts.map +1 -0
  55. package/dist/esm/datasource/fdv2/CacheInitializer.d.ts +17 -0
  56. package/dist/esm/datasource/fdv2/CacheInitializer.d.ts.map +1 -0
  57. package/dist/esm/datasource/fdv2/FDv1PollingSynchronizer.d.ts +2 -0
  58. package/dist/esm/datasource/fdv2/FDv1PollingSynchronizer.d.ts.map +1 -0
  59. package/dist/esm/datasource/fdv2/FDv2SourceResult.d.ts +3 -1
  60. package/dist/esm/datasource/fdv2/FDv2SourceResult.d.ts.map +1 -1
  61. package/dist/esm/datasource/fdv2/calculatePollDelay.d.ts +2 -0
  62. package/dist/esm/datasource/fdv2/calculatePollDelay.d.ts.map +1 -0
  63. package/dist/esm/datasource/fdv2/index.d.ts +4 -0
  64. package/dist/esm/datasource/fdv2/index.d.ts.map +1 -1
  65. package/dist/esm/datasource/flagEvalMapper.d.ts +3 -3
  66. package/dist/esm/flag-manager/FlagManager.d.ts +2 -1
  67. package/dist/esm/flag-manager/FlagManager.d.ts.map +1 -1
  68. package/dist/esm/flag-manager/FlagPersistence.d.ts +8 -1
  69. package/dist/esm/flag-manager/FlagPersistence.d.ts.map +1 -1
  70. package/dist/esm/index.d.ts +3 -1
  71. package/dist/esm/index.d.ts.map +1 -1
  72. package/dist/esm/index.mjs +329 -61
  73. package/dist/esm/index.mjs.map +1 -1
  74. package/dist/esm/storage/freshness.d.ts +27 -0
  75. package/dist/esm/storage/freshness.d.ts.map +1 -0
  76. package/dist/esm/storage/loadCachedFlags.d.ts +25 -0
  77. package/dist/esm/storage/loadCachedFlags.d.ts.map +1 -0
  78. package/package.json +1 -1
@@ -271,35 +271,141 @@ function validateOptions(input, validatorMap, defaults, logger, prefix) {
271
271
  });
272
272
  return result;
273
273
  }
274
+ /**
275
+ * Creates a validator for nested objects. When used in a validator map,
276
+ * `validateOptions` will recursively validate the nested object's properties.
277
+ * Defaults for nested fields are passed through from the parent.
278
+ */
279
+ function validatorOf(validators, builtInDefaults) {
280
+ return {
281
+ is: (u) => TypeValidators.Object.is(u),
282
+ getType: () => 'object',
283
+ validate(value, name, logger, defaults) {
284
+ if (!TypeValidators.Object.is(value)) {
285
+ logger?.warn(OptionMessages.wrongOptionType(name, 'object', typeof value));
286
+ return undefined;
287
+ }
288
+ const nestedDefaults = builtInDefaults ??
289
+ (TypeValidators.Object.is(defaults) ? defaults : {});
290
+ const nested = validateOptions(value, validators, nestedDefaults, logger, name);
291
+ return Object.keys(nested).length > 0 ? { value: nested } : undefined;
292
+ },
293
+ };
294
+ }
295
+ /**
296
+ * Creates a validator that tries each provided validator in order and uses the
297
+ * first one whose `is()` check passes. For compound validators the value is
298
+ * processed through `validate()`; for simple validators the value is accepted
299
+ * as-is. If no validator matches, a warning is logged and the default is
300
+ * preserved.
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * // Accepts either a boolean or a nested object with specific fields:
305
+ * anyOf(TypeValidators.Boolean, validatorOf({ lifecycle: TypeValidators.Boolean }))
306
+ * ```
307
+ */
308
+ function anyOf(...validators) {
309
+ return {
310
+ is: (u) => validators.some((v) => v.is(u)),
311
+ getType: () => validators.map((v) => v.getType()).join(' | '),
312
+ validate(value, name, logger, defaults) {
313
+ const match = validators.find((v) => v.is(value));
314
+ if (match) {
315
+ return isCompoundValidator(match)
316
+ ? match.validate(value, name, logger, defaults)
317
+ : { value };
318
+ }
319
+ logger?.warn(OptionMessages.wrongOptionType(name, this.getType(), typeof value));
320
+ return undefined;
321
+ },
322
+ };
323
+ }
274
324
 
275
- // eslint-disable-next-line max-classes-per-file
276
- const validators = {
277
- logger: TypeValidators.Object,
278
- maxCachedContexts: TypeValidators.numberWithMin(0),
279
- baseUri: TypeValidators.String,
280
- streamUri: TypeValidators.String,
281
- eventsUri: TypeValidators.String,
282
- capacity: TypeValidators.numberWithMin(1),
283
- diagnosticRecordingInterval: TypeValidators.numberWithMin(2),
284
- flushInterval: TypeValidators.numberWithMin(2),
285
- streamInitialReconnectDelay: TypeValidators.numberWithMin(0),
286
- allAttributesPrivate: TypeValidators.Boolean,
287
- debug: TypeValidators.Boolean,
288
- diagnosticOptOut: TypeValidators.Boolean,
289
- withReasons: TypeValidators.Boolean,
290
- sendEvents: TypeValidators.Boolean,
325
+ const dataSourceTypeValidator = TypeValidators.oneOf('cache', 'polling', 'streaming');
326
+ const connectionModeValidator = TypeValidators.oneOf('streaming', 'polling', 'offline', 'one-shot', 'background');
327
+ const endpointValidators = {
328
+ pollingBaseUri: TypeValidators.String,
329
+ streamingBaseUri: TypeValidators.String,
330
+ };
331
+ ({
332
+ type: dataSourceTypeValidator,
291
333
  pollInterval: TypeValidators.numberWithMin(30),
292
- useReport: TypeValidators.Boolean,
293
- privateAttributes: TypeValidators.StringArray,
294
- applicationInfo: TypeValidators.Object,
295
- wrapperName: TypeValidators.String,
296
- wrapperVersion: TypeValidators.String,
297
- payloadFilterKey: TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
298
- hooks: TypeValidators.createTypeArray('Hook[]', {}),
299
- inspectors: TypeValidators.createTypeArray('LDInspection', {}),
300
- cleanOldPersistentData: TypeValidators.Boolean,
334
+ endpoints: validatorOf(endpointValidators),
335
+ });
336
+ ({
337
+ type: dataSourceTypeValidator,
338
+ initialReconnectDelay: TypeValidators.numberWithMin(1),
339
+ endpoints: validatorOf(endpointValidators),
340
+ });
341
+
342
+ const modeSwitchingValidators = {
343
+ lifecycle: TypeValidators.Boolean,
344
+ network: TypeValidators.Boolean,
345
+ };
346
+ const dataSystemValidators = {
347
+ initialConnectionMode: connectionModeValidator,
348
+ backgroundConnectionMode: connectionModeValidator,
349
+ automaticModeSwitching: anyOf(TypeValidators.Boolean, validatorOf(modeSwitchingValidators)),
350
+ };
351
+ /**
352
+ * Default FDv2 data system configuration for browser SDKs.
353
+ */
354
+ const BROWSER_DATA_SYSTEM_DEFAULTS = {
355
+ initialConnectionMode: 'one-shot',
356
+ backgroundConnectionMode: undefined,
357
+ automaticModeSwitching: false,
358
+ };
359
+ /**
360
+ * Default FDv2 data system configuration for mobile (React Native) SDKs.
361
+ */
362
+ const MOBILE_DATA_SYSTEM_DEFAULTS = {
363
+ initialConnectionMode: 'streaming',
364
+ backgroundConnectionMode: 'background',
365
+ automaticModeSwitching: true,
366
+ };
367
+ /**
368
+ * Default FDv2 data system configuration for desktop SDKs (Electron, etc.).
369
+ */
370
+ const DESKTOP_DATA_SYSTEM_DEFAULTS = {
371
+ initialConnectionMode: 'streaming',
372
+ backgroundConnectionMode: undefined,
373
+ automaticModeSwitching: false,
301
374
  };
302
375
 
376
+ function createValidators(options) {
377
+ return {
378
+ logger: TypeValidators.Object,
379
+ maxCachedContexts: TypeValidators.numberWithMin(0),
380
+ baseUri: TypeValidators.String,
381
+ streamUri: TypeValidators.String,
382
+ eventsUri: TypeValidators.String,
383
+ capacity: TypeValidators.numberWithMin(1),
384
+ diagnosticRecordingInterval: TypeValidators.numberWithMin(2),
385
+ flushInterval: TypeValidators.numberWithMin(2),
386
+ streamInitialReconnectDelay: TypeValidators.numberWithMin(0),
387
+ allAttributesPrivate: TypeValidators.Boolean,
388
+ debug: TypeValidators.Boolean,
389
+ diagnosticOptOut: TypeValidators.Boolean,
390
+ withReasons: TypeValidators.Boolean,
391
+ sendEvents: TypeValidators.Boolean,
392
+ pollInterval: TypeValidators.numberWithMin(30),
393
+ useReport: TypeValidators.Boolean,
394
+ privateAttributes: TypeValidators.StringArray,
395
+ disableCache: TypeValidators.Boolean,
396
+ applicationInfo: TypeValidators.Object,
397
+ wrapperName: TypeValidators.String,
398
+ wrapperVersion: TypeValidators.String,
399
+ payloadFilterKey: TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
400
+ hooks: TypeValidators.createTypeArray('Hook[]', {}),
401
+ inspectors: TypeValidators.createTypeArray('LDInspection', {}),
402
+ cleanOldPersistentData: TypeValidators.Boolean,
403
+ dataSystem: options?.dataSystemDefaults
404
+ ? validatorOf(dataSystemValidators, options.dataSystemDefaults)
405
+ : TypeValidators.Object,
406
+ };
407
+ }
408
+
303
409
  const DEFAULT_POLLING_INTERVAL = 60 * 5;
304
410
  const DEFAULT_POLLING = 'https://clientsdk.launchdarkly.com';
305
411
  const DEFAULT_STREAM = 'https://clientstream.launchdarkly.com';
@@ -325,6 +431,7 @@ class ConfigurationImpl {
325
431
  // eslint-disable-next-line @typescript-eslint/naming-convention
326
432
  this.streamUri = DEFAULT_STREAM;
327
433
  this.maxCachedContexts = 5;
434
+ this.disableCache = false;
328
435
  this.capacity = 100;
329
436
  this.diagnosticRecordingInterval = 900;
330
437
  this.flushInterval = 30;
@@ -341,6 +448,9 @@ class ConfigurationImpl {
341
448
  this.hooks = [];
342
449
  this.inspectors = [];
343
450
  this.logger = ensureSafeLogger(pristineOptions.logger);
451
+ const validators = createValidators({
452
+ dataSystemDefaults: internalOptions.dataSystemDefaults,
453
+ });
344
454
  const validated = validateOptions(pristineOptions, validators, {}, this.logger);
345
455
  Object.entries(validated).forEach(([k, v]) => {
346
456
  if (k !== 'logger') {
@@ -693,6 +803,71 @@ class EventFactory extends internal.EventFactoryBase {
693
803
  }
694
804
  }
695
805
 
806
+ /**
807
+ * Suffix appended to context storage keys to form the freshness storage key.
808
+ */
809
+ const FRESHNESS_SUFFIX = '_freshness';
810
+ /**
811
+ * Computes a SHA-256 hash of the context's full canonical JSON.
812
+ * Returns `undefined` if the context cannot be serialized.
813
+ */
814
+ async function hashContext(crypto, context) {
815
+ const json = context.canonicalUnfilteredJson();
816
+ if (!json) {
817
+ return undefined;
818
+ }
819
+ return digest(crypto.createHash('sha256').update(json), 'base64');
820
+ }
821
+
822
+ function isValidFlag(value) {
823
+ return value !== null && typeof value === 'object' && typeof value.version === 'number';
824
+ }
825
+ /**
826
+ * Loads cached flag data from storage for the given context.
827
+ *
828
+ * Checks the current storage key first, then falls back to the legacy
829
+ * canonical key location (pre-10.3.1). Does NOT perform migration — the
830
+ * caller is responsible for migrating data if {@link CachedFlagData.fromLegacyKey}
831
+ * is true.
832
+ *
833
+ * @returns The cached flag data, or `undefined` on cache miss or parse error.
834
+ */
835
+ async function loadCachedFlags(storage, crypto, environmentNamespace, context, logger) {
836
+ const storageKey = await namespaceForContextData(crypto, environmentNamespace, context);
837
+ let flagsJson = await storage.get(storageKey);
838
+ let fromLegacyKey = false;
839
+ if (flagsJson === null || flagsJson === undefined) {
840
+ // Fallback: in version <10.3.1 flag data was stored under the canonical key.
841
+ flagsJson = await storage.get(context.canonicalKey);
842
+ if (flagsJson === null || flagsJson === undefined) {
843
+ return undefined;
844
+ }
845
+ fromLegacyKey = true;
846
+ }
847
+ try {
848
+ const parsed = JSON.parse(flagsJson);
849
+ if (parsed === null || typeof parsed !== 'object') {
850
+ logger?.warn('Cached flag data is not a valid object');
851
+ return undefined;
852
+ }
853
+ const entries = Object.entries(parsed);
854
+ const invalidKey = entries.find(([, value]) => !isValidFlag(value));
855
+ if (invalidKey) {
856
+ logger?.warn(`Discarding cached flags due to invalid entry: ${invalidKey[0]}`);
857
+ return undefined;
858
+ }
859
+ const flags = entries.reduce((acc, [key, value]) => {
860
+ acc[key] = value;
861
+ return acc;
862
+ }, {});
863
+ return { flags, storageKey, fromLegacyKey };
864
+ }
865
+ catch (e) {
866
+ logger?.warn(`Could not parse cached flag evaluations from persistent storage: ${e.message}`);
867
+ return undefined;
868
+ }
869
+ }
870
+
696
871
  /**
697
872
  * An index for tracking the most recently used contexts by timestamp with the ability to
698
873
  * update entry timestamps and prune out least used contexts above a max capacity provided.
@@ -758,12 +933,18 @@ class ContextIndex {
758
933
  * This class handles persisting and loading flag values from a persistent
759
934
  * store. It intercepts updates and forwards them to the flag updater and
760
935
  * then persists changes after the updater has completed.
936
+ *
937
+ * Freshness metadata (timestamp + context attribute hash) is stored in a
938
+ * separate storage key (`{contextKey}_freshness`) alongside the flag data.
939
+ * Both keys are managed together — when a context is evicted, both the flag
940
+ * data and freshness record are cleared.
761
941
  */
762
942
  class FlagPersistence {
763
- constructor(_platform, _environmentNamespace, _maxCachedContexts, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
943
+ constructor(_platform, _environmentNamespace, _maxCachedContexts, _disableCache, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
764
944
  this._platform = _platform;
765
945
  this._environmentNamespace = _environmentNamespace;
766
946
  this._maxCachedContexts = _maxCachedContexts;
947
+ this._disableCache = _disableCache;
767
948
  this._flagStore = _flagStore;
768
949
  this._flagUpdater = _flagUpdater;
769
950
  this._logger = _logger;
@@ -795,35 +976,38 @@ class FlagPersistence {
795
976
  * {@link FlagUpdater} this {@link FlagPersistence} was constructed with.
796
977
  */
797
978
  async loadCached(context) {
798
- const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
799
- let flagsJson = await this._platform.storage?.get(storageKey);
800
- if (flagsJson === null || flagsJson === undefined) {
801
- // Fallback: in version <10.3.1 flag data was stored under the canonical key, check
802
- // to see if data is present and migrate the data if present.
803
- flagsJson = await this._platform.storage?.get(context.canonicalKey);
804
- if (flagsJson === null || flagsJson === undefined) {
805
- // return false indicating cache did not load if flag json is still absent
806
- return false;
807
- }
808
- // migrate data from version <10.3.1 and cleanup data that was under canonical key
809
- await this._platform.storage?.set(storageKey, flagsJson);
810
- await this._platform.storage?.clear(context.canonicalKey);
979
+ if (this._disableCache || this._maxCachedContexts <= 0) {
980
+ return false;
811
981
  }
812
- try {
813
- const flags = JSON.parse(flagsJson);
814
- // mapping flags to item descriptors
815
- const descriptors = Object.entries(flags).reduce((acc, [key, flag]) => {
816
- acc[key] = { version: flag.version, flag };
817
- return acc;
818
- }, {});
819
- this._flagUpdater.initCached(context, descriptors);
820
- this._logger.debug('Loaded cached flag evaluations from persistent storage');
821
- return true;
982
+ if (!this._platform.storage) {
983
+ return false;
822
984
  }
823
- catch (e) {
824
- this._logger.warn(`Could not load cached flag evaluations from persistent storage: ${e.message}`);
985
+ const cached = await loadCachedFlags(this._platform.storage, this._platform.crypto, this._environmentNamespace, context, this._logger);
986
+ if (!cached) {
825
987
  return false;
826
988
  }
989
+ // Migrate data from version <10.3.1 stored under the canonical key
990
+ if (cached.fromLegacyKey) {
991
+ await this._platform.storage.set(cached.storageKey, JSON.stringify(cached.flags));
992
+ await this._platform.storage.clear(context.canonicalKey);
993
+ }
994
+ // mapping flags to item descriptors
995
+ const descriptors = Object.entries(cached.flags).reduce((acc, [key, flag]) => {
996
+ acc[key] = { version: flag.version, flag };
997
+ return acc;
998
+ }, {});
999
+ this._flagUpdater.initCached(context, descriptors);
1000
+ this._logger.debug('Loaded cached flag evaluations from persistent storage');
1001
+ return true;
1002
+ }
1003
+ async _storeFreshness(contextStorageKey, context, timestamp) {
1004
+ const contextHash = await hashContext(this._platform.crypto, context);
1005
+ if (contextHash === undefined) {
1006
+ this._logger.error('Could not serialize context for freshness tracking');
1007
+ return;
1008
+ }
1009
+ const record = { timestamp, contextHash };
1010
+ await this._platform.storage?.set(`${contextStorageKey}${FRESHNESS_SUFFIX}`, JSON.stringify(record));
827
1011
  }
828
1012
  async _loadIndex() {
829
1013
  if (this._contextIndex !== undefined) {
@@ -845,13 +1029,26 @@ class FlagPersistence {
845
1029
  return this._contextIndex;
846
1030
  }
847
1031
  async _storeCache(context) {
1032
+ if (this._disableCache) {
1033
+ return;
1034
+ }
1035
+ const now = this._timeStamper();
848
1036
  const index = await this._loadIndex();
849
1037
  const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
850
- index.notice(storageKey, this._timeStamper());
1038
+ if (this._maxCachedContexts > 0) {
1039
+ index.notice(storageKey, now);
1040
+ }
851
1041
  const pruned = index.prune(this._maxCachedContexts);
852
- await Promise.all(pruned.map(async (it) => this._platform.storage?.clear(it.id)));
853
- // store index
1042
+ // If maxCachedContexts <= 0, current context was never added, so always skip flag write
1043
+ const currentContextWasPruned = this._maxCachedContexts <= 0 || pruned.some((it) => it.id === storageKey);
1044
+ await Promise.all(pruned.flatMap((it) => [
1045
+ this._platform.storage?.clear(it.id),
1046
+ this._platform.storage?.clear(`${it.id}${FRESHNESS_SUFFIX}`),
1047
+ ]));
854
1048
  await this._platform.storage?.set(await this._indexKeyPromise, index.toJson());
1049
+ if (currentContextWasPruned) {
1050
+ return;
1051
+ }
855
1052
  const allFlags = this._flagStore.getAll();
856
1053
  // mapping item descriptors to flags
857
1054
  const flags = Object.entries(allFlags).reduce((acc, [key, descriptor]) => {
@@ -861,8 +1058,15 @@ class FlagPersistence {
861
1058
  return acc;
862
1059
  }, {});
863
1060
  const jsonAll = JSON.stringify(flags);
864
- // store flag data
1061
+ // store flag data first, so freshness is never newer than the flags it describes
865
1062
  await this._platform.storage?.set(storageKey, jsonAll);
1063
+ // store freshness — best-effort, must not block flag persistence
1064
+ try {
1065
+ await this._storeFreshness(storageKey, context, now);
1066
+ }
1067
+ catch (e) {
1068
+ this._logger.warn(`Failed to store freshness data: ${e.message}`);
1069
+ }
866
1070
  }
867
1071
  }
868
1072
 
@@ -978,17 +1182,18 @@ class DefaultFlagManager {
978
1182
  * @param platform implementation of various platform provided functionality
979
1183
  * @param sdkKey that will be used to distinguish different environments
980
1184
  * @param maxCachedContexts that specifies the max number of contexts that will be cached in persistence
1185
+ * @param disableCache set to true to completely disable the persistent flag cache
981
1186
  * @param logger used for logging various messages
982
1187
  * @param timeStamper exists for testing purposes
983
1188
  */
984
- constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
1189
+ constructor(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper = () => Date.now()) {
985
1190
  this._flagStore = createDefaultFlagStore();
986
1191
  this._flagUpdater = createFlagUpdater(this._flagStore, logger);
987
- this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
1192
+ this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper);
988
1193
  }
989
- async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
1194
+ async _initPersistence(platform, sdkKey, maxCachedContexts, disableCache, logger, timeStamper = () => Date.now()) {
990
1195
  const environmentNamespace = await namespaceForEnvironment(platform.crypto, sdkKey);
991
- return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
1196
+ return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, disableCache, this._flagStore, this._flagUpdater, logger, timeStamper);
992
1197
  }
993
1198
  get(key) {
994
1199
  if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
@@ -1464,7 +1669,7 @@ class LDClientImpl {
1464
1669
  this._config = new ConfigurationImpl(options, internalOptions);
1465
1670
  this.logger = this._config.logger;
1466
1671
  this._baseHeaders = defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
1467
- this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.logger);
1672
+ this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.disableCache ?? false, this._config.logger);
1468
1673
  this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
1469
1674
  this._eventProcessor = createEventProcessor(sdkKey, this._config, platform, this._baseHeaders, this._diagnosticsManager);
1470
1675
  this.emitter = new LDEmitter();
@@ -2558,5 +2763,68 @@ class BaseDataManager {
2558
2763
  }
2559
2764
  }
2560
2765
 
2561
- export { BaseDataManager, DataSourceState, LDClientImpl, browserFdv1Endpoints, fdv2Endpoints, makeRequestor, mobileFdv1Endpoints, readFlagsFromBootstrap, safeRegisterDebugOverridePlugins, validateOptions };
2766
+ function allConditionsMatch(conditions, input) {
2767
+ return Object.entries(conditions).every(([key, value]) => value === undefined || input[key] === value);
2768
+ }
2769
+ /**
2770
+ * Given a mode resolution table and the current input state, returns the
2771
+ * resolved FDv2 connection mode.
2772
+ *
2773
+ * Iterates entries in order. The first entry whose conditions all match the
2774
+ * input wins. If no entry matches (should not happen when the table ends with
2775
+ * a catch-all), falls back to `input.foregroundMode`.
2776
+ */
2777
+ const CONFIGURED_MODE_MAP = {
2778
+ foreground: 'foregroundMode',
2779
+ background: 'backgroundMode',
2780
+ };
2781
+ function resolveConnectionMode(table, input) {
2782
+ const match = table.find((entry) => allConditionsMatch(entry.conditions, input));
2783
+ if (match) {
2784
+ const { mode } = match;
2785
+ if (typeof mode === 'object') {
2786
+ return input[CONFIGURED_MODE_MAP[mode.configured]];
2787
+ }
2788
+ return mode;
2789
+ }
2790
+ return input.foregroundMode;
2791
+ }
2792
+ /**
2793
+ * Mode resolution table for mobile platforms (React Native, etc.).
2794
+ *
2795
+ * - No network → offline.
2796
+ * - Background → configured background mode.
2797
+ * - Foreground → configured foreground mode.
2798
+ */
2799
+ const MOBILE_TRANSITION_TABLE = [
2800
+ { conditions: { networkAvailable: false }, mode: 'offline' },
2801
+ { conditions: { lifecycle: 'background' }, mode: { configured: 'background' } },
2802
+ { conditions: { lifecycle: 'foreground' }, mode: { configured: 'foreground' } },
2803
+ ];
2804
+ /**
2805
+ * Mode resolution table for browser platforms.
2806
+ *
2807
+ * - No network → offline.
2808
+ * - Otherwise → configured foreground mode.
2809
+ *
2810
+ * Browser listener-driven streaming (auto-promotion to streaming when change
2811
+ * listeners are registered) is handled externally by the caller modifying
2812
+ * `foregroundMode` before consulting this table.
2813
+ */
2814
+ const BROWSER_TRANSITION_TABLE = [
2815
+ { conditions: { networkAvailable: false }, mode: 'offline' },
2816
+ { conditions: {}, mode: { configured: 'foreground' } },
2817
+ ];
2818
+ /**
2819
+ * Mode resolution table for desktop platforms (Electron, etc.).
2820
+ *
2821
+ * - No network → offline.
2822
+ * - Otherwise → configured foreground mode.
2823
+ */
2824
+ const DESKTOP_TRANSITION_TABLE = [
2825
+ { conditions: { networkAvailable: false }, mode: 'offline' },
2826
+ { conditions: {}, mode: { configured: 'foreground' } },
2827
+ ];
2828
+
2829
+ export { BROWSER_DATA_SYSTEM_DEFAULTS, BROWSER_TRANSITION_TABLE, BaseDataManager, DESKTOP_DATA_SYSTEM_DEFAULTS, DESKTOP_TRANSITION_TABLE, DataSourceState, LDClientImpl, MOBILE_DATA_SYSTEM_DEFAULTS, MOBILE_TRANSITION_TABLE, browserFdv1Endpoints, dataSystemValidators, fdv2Endpoints, makeRequestor, mobileFdv1Endpoints, readFlagsFromBootstrap, resolveConnectionMode, safeRegisterDebugOverridePlugins, validateOptions };
2562
2830
  //# sourceMappingURL=index.mjs.map