@launchdarkly/js-client-sdk-common 1.15.2 → 1.17.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 (68) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cjs/LDClientImpl.d.ts +37 -4
  3. package/dist/cjs/LDClientImpl.d.ts.map +1 -1
  4. package/dist/cjs/LDEmitter.d.ts +1 -1
  5. package/dist/cjs/LDEmitter.d.ts.map +1 -1
  6. package/dist/cjs/api/LDClient.d.ts +33 -0
  7. package/dist/cjs/api/LDClient.d.ts.map +1 -1
  8. package/dist/cjs/api/LDOptions.d.ts +7 -0
  9. package/dist/cjs/api/LDOptions.d.ts.map +1 -1
  10. package/dist/cjs/api/LDPlugin.d.ts +16 -0
  11. package/dist/cjs/api/LDPlugin.d.ts.map +1 -0
  12. package/dist/cjs/api/LDWaitForInitialization.d.ts +50 -0
  13. package/dist/cjs/api/LDWaitForInitialization.d.ts.map +1 -0
  14. package/dist/cjs/api/index.d.ts +2 -0
  15. package/dist/cjs/api/index.d.ts.map +1 -1
  16. package/dist/cjs/configuration/Configuration.d.ts +1 -0
  17. package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
  18. package/dist/cjs/configuration/validators.d.ts.map +1 -1
  19. package/dist/cjs/context/createActiveContextTracker.d.ts +49 -0
  20. package/dist/cjs/context/createActiveContextTracker.d.ts.map +1 -0
  21. package/dist/cjs/flag-manager/FlagManager.d.ts +70 -1
  22. package/dist/cjs/flag-manager/FlagManager.d.ts.map +1 -1
  23. package/dist/cjs/flag-manager/FlagPersistence.d.ts +1 -1
  24. package/dist/cjs/flag-manager/FlagPersistence.d.ts.map +1 -1
  25. package/dist/cjs/flag-manager/FlagStore.d.ts +15 -13
  26. package/dist/cjs/flag-manager/FlagStore.d.ts.map +1 -1
  27. package/dist/cjs/flag-manager/FlagUpdater.d.ts +35 -7
  28. package/dist/cjs/flag-manager/FlagUpdater.d.ts.map +1 -1
  29. package/dist/cjs/index.cjs +347 -121
  30. package/dist/cjs/index.cjs.map +1 -1
  31. package/dist/cjs/index.d.ts +3 -2
  32. package/dist/cjs/index.d.ts.map +1 -1
  33. package/dist/cjs/plugins/safeRegisterDebugOverridePlugins.d.ts +12 -0
  34. package/dist/cjs/plugins/safeRegisterDebugOverridePlugins.d.ts.map +1 -0
  35. package/dist/esm/LDClientImpl.d.ts +37 -4
  36. package/dist/esm/LDClientImpl.d.ts.map +1 -1
  37. package/dist/esm/LDEmitter.d.ts +1 -1
  38. package/dist/esm/LDEmitter.d.ts.map +1 -1
  39. package/dist/esm/api/LDClient.d.ts +33 -0
  40. package/dist/esm/api/LDClient.d.ts.map +1 -1
  41. package/dist/esm/api/LDOptions.d.ts +7 -0
  42. package/dist/esm/api/LDOptions.d.ts.map +1 -1
  43. package/dist/esm/api/LDPlugin.d.ts +16 -0
  44. package/dist/esm/api/LDPlugin.d.ts.map +1 -0
  45. package/dist/esm/api/LDWaitForInitialization.d.ts +50 -0
  46. package/dist/esm/api/LDWaitForInitialization.d.ts.map +1 -0
  47. package/dist/esm/api/index.d.ts +2 -0
  48. package/dist/esm/api/index.d.ts.map +1 -1
  49. package/dist/esm/configuration/Configuration.d.ts +1 -0
  50. package/dist/esm/configuration/Configuration.d.ts.map +1 -1
  51. package/dist/esm/configuration/validators.d.ts.map +1 -1
  52. package/dist/esm/context/createActiveContextTracker.d.ts +49 -0
  53. package/dist/esm/context/createActiveContextTracker.d.ts.map +1 -0
  54. package/dist/esm/flag-manager/FlagManager.d.ts +70 -1
  55. package/dist/esm/flag-manager/FlagManager.d.ts.map +1 -1
  56. package/dist/esm/flag-manager/FlagPersistence.d.ts +1 -1
  57. package/dist/esm/flag-manager/FlagPersistence.d.ts.map +1 -1
  58. package/dist/esm/flag-manager/FlagStore.d.ts +15 -13
  59. package/dist/esm/flag-manager/FlagStore.d.ts.map +1 -1
  60. package/dist/esm/flag-manager/FlagUpdater.d.ts +35 -7
  61. package/dist/esm/flag-manager/FlagUpdater.d.ts.map +1 -1
  62. package/dist/esm/index.d.ts +3 -2
  63. package/dist/esm/index.d.ts.map +1 -1
  64. package/dist/esm/index.mjs +348 -123
  65. package/dist/esm/index.mjs.map +1 -1
  66. package/dist/esm/plugins/safeRegisterDebugOverridePlugins.d.ts +12 -0
  67. package/dist/esm/plugins/safeRegisterDebugOverridePlugins.d.ts.map +1 -0
  68. package/package.json +2 -2
@@ -260,6 +260,7 @@ const validators = {
260
260
  payloadFilterKey: jsSdkCommon.TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
261
261
  hooks: jsSdkCommon.TypeValidators.createTypeArray('Hook[]', {}),
262
262
  inspectors: jsSdkCommon.TypeValidators.createTypeArray('LDInspection', {}),
263
+ cleanOldPersistentData: jsSdkCommon.TypeValidators.Boolean,
263
264
  };
264
265
 
265
266
  const DEFAULT_POLLING_INTERVAL = 60 * 5;
@@ -538,6 +539,38 @@ const addAutoEnv = async (context, platform, config) => {
538
539
  return context;
539
540
  };
540
541
 
542
+ function createActiveContextTracker() {
543
+ let unwrappedContext;
544
+ let context;
545
+ return {
546
+ set(_unwrappedContext, _context) {
547
+ unwrappedContext = _unwrappedContext;
548
+ context = _context;
549
+ },
550
+ getContext() {
551
+ return context;
552
+ },
553
+ getUnwrappedContext() {
554
+ return unwrappedContext;
555
+ },
556
+ newIdentificationPromise() {
557
+ let res;
558
+ let rej;
559
+ const basePromise = new Promise((resolve, reject) => {
560
+ res = resolve;
561
+ rej = reject;
562
+ });
563
+ return { identifyPromise: basePromise, identifyResolve: res, identifyReject: rej };
564
+ },
565
+ hasContext() {
566
+ return context !== undefined;
567
+ },
568
+ hasValidContext() {
569
+ return this.hasContext() && context.valid;
570
+ },
571
+ };
572
+ }
573
+
541
574
  const { isLegacyUser, isMultiKind, isSingleKind } = jsSdkCommon.internal;
542
575
  /**
543
576
  * This is the root ensureKey function. All other ensureKey functions reduce to this.
@@ -831,30 +864,30 @@ class FlagPersistence {
831
864
  }
832
865
 
833
866
  /**
834
- * In memory flag store.
867
+ * Creates the default implementation of the flag store.
835
868
  */
836
- class DefaultFlagStore {
837
- constructor() {
838
- this._flags = {};
839
- }
840
- init(newFlags) {
841
- this._flags = Object.entries(newFlags).reduce((acc, [key, flag]) => {
842
- acc[key] = flag;
843
- return acc;
844
- }, {});
845
- }
846
- insertOrUpdate(key, update) {
847
- this._flags[key] = update;
848
- }
849
- get(key) {
850
- if (Object.prototype.hasOwnProperty.call(this._flags, key)) {
851
- return this._flags[key];
852
- }
853
- return undefined;
854
- }
855
- getAll() {
856
- return this._flags;
857
- }
869
+ function createDefaultFlagStore() {
870
+ let flags = {};
871
+ return {
872
+ init(newFlags) {
873
+ flags = Object.entries(newFlags).reduce((acc, [key, flag]) => {
874
+ acc[key] = flag;
875
+ return acc;
876
+ }, {});
877
+ },
878
+ insertOrUpdate(key, update) {
879
+ flags[key] = update;
880
+ },
881
+ get(key) {
882
+ if (Object.prototype.hasOwnProperty.call(flags, key)) {
883
+ return flags[key];
884
+ }
885
+ return undefined;
886
+ },
887
+ getAll() {
888
+ return flags;
889
+ },
890
+ };
858
891
  }
859
892
 
860
893
  function calculateChangedKeys(existingObject, newObject) {
@@ -875,69 +908,66 @@ function calculateChangedKeys(existingObject, newObject) {
875
908
  return changedKeys;
876
909
  }
877
910
 
878
- /**
879
- * The flag updater handles logic required during the flag update process.
880
- * It handles versions checking to handle out of order flag updates and
881
- * also handles flag comparisons for change notification.
882
- */
883
- class FlagUpdater {
884
- constructor(flagStore, logger) {
885
- this._changeCallbacks = new Array();
886
- this._flagStore = flagStore;
887
- this._logger = logger;
888
- }
889
- init(context, newFlags) {
890
- this._activeContextKey = context.canonicalKey;
891
- const oldFlags = this._flagStore.getAll();
892
- this._flagStore.init(newFlags);
893
- const changed = calculateChangedKeys(oldFlags, newFlags);
894
- if (changed.length > 0) {
895
- this._changeCallbacks.forEach((callback) => {
896
- try {
897
- callback(context, changed, 'init');
898
- }
899
- catch (err) {
900
- /* intentionally empty */
901
- }
902
- });
903
- }
904
- }
905
- initCached(context, newFlags) {
906
- if (this._activeContextKey === context.canonicalKey) {
907
- return;
908
- }
909
- this.init(context, newFlags);
910
- }
911
- upsert(context, key, item) {
912
- if (this._activeContextKey !== context.canonicalKey) {
913
- this._logger.warn('Received an update for an inactive context.');
914
- return false;
915
- }
916
- const currentValue = this._flagStore.get(key);
917
- if (currentValue !== undefined && currentValue.version >= item.version) {
918
- // this is an out of order update that can be ignored
919
- return false;
920
- }
921
- this._flagStore.insertOrUpdate(key, item);
922
- this._changeCallbacks.forEach((callback) => {
923
- try {
924
- callback(context, [key], 'patch');
911
+ function createFlagUpdater(_flagStore, _logger) {
912
+ const flagStore = _flagStore;
913
+ const logger = _logger;
914
+ let activeContext;
915
+ const changeCallbacks = new Array();
916
+ return {
917
+ handleFlagChanges(keys, type) {
918
+ if (activeContext) {
919
+ changeCallbacks.forEach((callback) => {
920
+ try {
921
+ callback(activeContext, keys, type);
922
+ }
923
+ catch (err) {
924
+ /* intentionally empty */
925
+ }
926
+ });
925
927
  }
926
- catch (err) {
927
- /* intentionally empty */
928
+ else {
929
+ logger.warn('Received a change event without an active context. Changes will not be propagated.');
928
930
  }
929
- });
930
- return true;
931
- }
932
- on(callback) {
933
- this._changeCallbacks.push(callback);
934
- }
935
- off(callback) {
936
- const index = this._changeCallbacks.indexOf(callback);
937
- if (index > -1) {
938
- this._changeCallbacks.splice(index, 1);
939
- }
940
- }
931
+ },
932
+ init(context, newFlags) {
933
+ activeContext = context;
934
+ const oldFlags = flagStore.getAll();
935
+ flagStore.init(newFlags);
936
+ const changed = calculateChangedKeys(oldFlags, newFlags);
937
+ if (changed.length > 0) {
938
+ this.handleFlagChanges(changed, 'init');
939
+ }
940
+ },
941
+ initCached(context, newFlags) {
942
+ if (activeContext?.canonicalKey === context.canonicalKey) {
943
+ return;
944
+ }
945
+ this.init(context, newFlags);
946
+ },
947
+ upsert(context, key, item) {
948
+ if (activeContext?.canonicalKey !== context.canonicalKey) {
949
+ logger.warn('Received an update for an inactive context.');
950
+ return false;
951
+ }
952
+ const currentValue = flagStore.get(key);
953
+ if (currentValue !== undefined && currentValue.version >= item.version) {
954
+ // this is an out of order update that can be ignored
955
+ return false;
956
+ }
957
+ flagStore.insertOrUpdate(key, item);
958
+ this.handleFlagChanges([key], 'patch');
959
+ return true;
960
+ },
961
+ on(callback) {
962
+ changeCallbacks.push(callback);
963
+ },
964
+ off(callback) {
965
+ const index = changeCallbacks.indexOf(callback);
966
+ if (index > -1) {
967
+ changeCallbacks.splice(index, 1);
968
+ }
969
+ },
970
+ };
941
971
  }
942
972
 
943
973
  class DefaultFlagManager {
@@ -949,8 +979,8 @@ class DefaultFlagManager {
949
979
  * @param timeStamper exists for testing purposes
950
980
  */
951
981
  constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
952
- this._flagStore = new DefaultFlagStore();
953
- this._flagUpdater = new FlagUpdater(this._flagStore, logger);
982
+ this._flagStore = createDefaultFlagStore();
983
+ this._flagUpdater = createFlagUpdater(this._flagStore, logger);
954
984
  this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
955
985
  }
956
986
  async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
@@ -958,11 +988,26 @@ class DefaultFlagManager {
958
988
  return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
959
989
  }
960
990
  get(key) {
991
+ if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
992
+ return this._convertValueToOverrideDescripter(this._overrides[key]);
993
+ }
961
994
  return this._flagStore.get(key);
962
995
  }
963
996
  getAll() {
997
+ if (this._overrides) {
998
+ return {
999
+ ...this._flagStore.getAll(),
1000
+ ...Object.entries(this._overrides).reduce((acc, [key, value]) => {
1001
+ acc[key] = this._convertValueToOverrideDescripter(value);
1002
+ return acc;
1003
+ }, {}),
1004
+ };
1005
+ }
964
1006
  return this._flagStore.getAll();
965
1007
  }
1008
+ presetFlags(newFlags) {
1009
+ this._flagStore.init(newFlags);
1010
+ }
966
1011
  setBootstrap(context, newFlags) {
967
1012
  // Bypasses the persistence as we do not want to put these flags into any cache.
968
1013
  // Generally speaking persistence likely *SHOULD* be disabled when using bootstrap.
@@ -983,6 +1028,58 @@ class DefaultFlagManager {
983
1028
  off(callback) {
984
1029
  this._flagUpdater.off(callback);
985
1030
  }
1031
+ _convertValueToOverrideDescripter(value) {
1032
+ return {
1033
+ flag: {
1034
+ value,
1035
+ version: 0,
1036
+ },
1037
+ version: 0,
1038
+ };
1039
+ }
1040
+ setOverride(key, value) {
1041
+ if (!this._overrides) {
1042
+ this._overrides = {};
1043
+ }
1044
+ this._overrides[key] = value;
1045
+ this._flagUpdater.handleFlagChanges([key], 'override');
1046
+ }
1047
+ removeOverride(flagKey) {
1048
+ if (!this._overrides || !Object.prototype.hasOwnProperty.call(this._overrides, flagKey)) {
1049
+ return; // No override to remove
1050
+ }
1051
+ delete this._overrides[flagKey];
1052
+ // If no more overrides, reset to undefined for performance
1053
+ if (Object.keys(this._overrides).length === 0) {
1054
+ this._overrides = undefined;
1055
+ }
1056
+ this._flagUpdater.handleFlagChanges([flagKey], 'override');
1057
+ }
1058
+ clearAllOverrides() {
1059
+ if (this._overrides) {
1060
+ const clearedOverrides = { ...this._overrides };
1061
+ this._overrides = undefined; // Reset to undefined
1062
+ this._flagUpdater.handleFlagChanges(Object.keys(clearedOverrides), 'override');
1063
+ }
1064
+ }
1065
+ getAllOverrides() {
1066
+ if (!this._overrides) {
1067
+ return {};
1068
+ }
1069
+ const result = {};
1070
+ Object.entries(this._overrides).forEach(([key, value]) => {
1071
+ result[key] = this._convertValueToOverrideDescripter(value);
1072
+ });
1073
+ return result;
1074
+ }
1075
+ getDebugOverride() {
1076
+ return {
1077
+ setOverride: this.setOverride.bind(this),
1078
+ removeOverride: this.removeOverride.bind(this),
1079
+ clearAllOverrides: this.clearAllOverrides.bind(this),
1080
+ getAllOverrides: this.getAllOverrides.bind(this),
1081
+ };
1082
+ }
986
1083
  }
987
1084
 
988
1085
  const UNKNOWN_HOOK_NAME = 'unknown hook';
@@ -1345,6 +1442,7 @@ class LDClientImpl {
1345
1442
  this.sdkKey = sdkKey;
1346
1443
  this.autoEnvAttributes = autoEnvAttributes;
1347
1444
  this.platform = platform;
1445
+ this._activeContextTracker = createActiveContextTracker();
1348
1446
  this._highTimeoutThreshold = 15;
1349
1447
  this._eventFactoryDefault = new EventFactory(false);
1350
1448
  this._eventFactoryWithReasons = new EventFactory(true);
@@ -1385,6 +1483,24 @@ class LDClientImpl {
1385
1483
  if (this._inspectorManager.hasInspectors()) {
1386
1484
  this._hookRunner.addHook(getInspectorHook(this._inspectorManager));
1387
1485
  }
1486
+ if (options.cleanOldPersistentData &&
1487
+ internalOptions?.getLegacyStorageKeys &&
1488
+ this.platform.storage) {
1489
+ // NOTE: we are letting this fail silently because it's not critical and we don't want to block the client from initializing.
1490
+ try {
1491
+ this.logger.debug('Cleaning old persistent data.');
1492
+ Promise.all(internalOptions.getLegacyStorageKeys().map((key) => this.platform.storage?.clear(key)))
1493
+ .catch((error) => {
1494
+ this.logger.error(`Error cleaning old persistent data: ${error}`);
1495
+ })
1496
+ .finally(() => {
1497
+ this.logger.debug('Cleaned old persistent data.');
1498
+ });
1499
+ }
1500
+ catch (error) {
1501
+ this.logger.error(`Error cleaning old persistent data: ${error}`);
1502
+ }
1503
+ }
1388
1504
  }
1389
1505
  allFlags() {
1390
1506
  // extracting all flag values
@@ -1419,19 +1535,20 @@ class LDClientImpl {
1419
1535
  // code. We are returned the unchecked context so that if a consumer identifies with an invalid context
1420
1536
  // and then calls getContext, they get back the same context they provided, without any assertion about
1421
1537
  // validity.
1422
- return this._uncheckedContext ? jsSdkCommon.clone(this._uncheckedContext) : undefined;
1538
+ return this._activeContextTracker.hasContext()
1539
+ ? jsSdkCommon.clone(this._activeContextTracker.getUnwrappedContext())
1540
+ : undefined;
1423
1541
  }
1424
1542
  getInternalContext() {
1425
- return this._checkedContext;
1426
- }
1427
- _createIdentifyPromise() {
1428
- let res;
1429
- let rej;
1430
- const basePromise = new Promise((resolve, reject) => {
1431
- res = resolve;
1432
- rej = reject;
1433
- });
1434
- return { identifyPromise: basePromise, identifyResolve: res, identifyReject: rej };
1543
+ return this._activeContextTracker.getContext();
1544
+ }
1545
+ /**
1546
+ * Preset flags are used to set the flags before the client is initialized. This is useful for
1547
+ * when client has precached flags that are ready to evaluate without full initialization.
1548
+ * @param newFlags - The flags to preset.
1549
+ */
1550
+ presetFlags(newFlags) {
1551
+ this._flagManager.presetFlags(newFlags);
1435
1552
  }
1436
1553
  /**
1437
1554
  * Identifies a context to LaunchDarkly. See {@link LDClient.identify}.
@@ -1507,11 +1624,10 @@ class LDClientImpl {
1507
1624
  this.emitter.emit('error', context, error);
1508
1625
  return Promise.reject(error);
1509
1626
  }
1510
- this._uncheckedContext = context;
1511
- this._checkedContext = checkedContext;
1512
- this._eventProcessor?.sendEvent(this._eventFactoryDefault.identifyEvent(this._checkedContext));
1513
- const { identifyPromise, identifyResolve, identifyReject } = this._createIdentifyPromise();
1514
- this.logger.debug(`Identifying ${JSON.stringify(this._checkedContext)}`);
1627
+ this._activeContextTracker.set(context, checkedContext);
1628
+ this._eventProcessor?.sendEvent(this._eventFactoryDefault.identifyEvent(checkedContext));
1629
+ const { identifyPromise, identifyResolve, identifyReject } = this._activeContextTracker.newIdentificationPromise();
1630
+ this.logger.debug(`Identifying ${JSON.stringify(checkedContext)}`);
1515
1631
  await this.dataManager.identify(identifyResolve, identifyReject, checkedContext, identifyOptions);
1516
1632
  return identifyPromise;
1517
1633
  },
@@ -1529,12 +1645,18 @@ class LDClientImpl {
1529
1645
  }, identifyOptions?.sheddable ?? false)
1530
1646
  .then((res) => {
1531
1647
  if (res.status === 'error') {
1532
- return { status: 'error', error: res.error };
1648
+ const errorResult = { status: 'error', error: res.error };
1649
+ // Track initialization state for waitForInitialization
1650
+ this.maybeSetInitializationResult({ status: 'failed', error: res.error });
1651
+ return errorResult;
1533
1652
  }
1534
1653
  if (res.status === 'shed') {
1535
1654
  return { status: 'shed' };
1536
1655
  }
1537
- return { status: 'completed' };
1656
+ const successResult = { status: 'completed' };
1657
+ // Track initialization state for waitForInitialization
1658
+ this.maybeSetInitializationResult({ status: 'complete' });
1659
+ return successResult;
1538
1660
  });
1539
1661
  if (noTimeout) {
1540
1662
  return callSitePromise;
@@ -1546,6 +1668,74 @@ class LDClientImpl {
1546
1668
  });
1547
1669
  return Promise.race([callSitePromise, timeoutPromise]);
1548
1670
  }
1671
+ /**
1672
+ * Sets the initialization result and resolves any pending waitForInitialization promises.
1673
+ * This method is idempotent and will only be set by the initialization flow. Subsequent calls
1674
+ * should not do anything.
1675
+ * @param result The initialization result.
1676
+ */
1677
+ maybeSetInitializationResult(result) {
1678
+ if (this.initializeResult === undefined) {
1679
+ this.initializeResult = result;
1680
+ this.emitter.emit('ready');
1681
+ if (result.status === 'complete') {
1682
+ this.emitter.emit('initialized');
1683
+ }
1684
+ if (this.initResolve) {
1685
+ this.initResolve(result);
1686
+ this.initResolve = undefined;
1687
+ }
1688
+ }
1689
+ }
1690
+ waitForInitialization(options) {
1691
+ const timeout = options?.timeout ?? 5;
1692
+ // If initialization has already completed (successfully or failed), return the result immediately.
1693
+ if (this.initializeResult) {
1694
+ return Promise.resolve(this.initializeResult);
1695
+ }
1696
+ // If waitForInitialization was previously called, then return the promise with a timeout.
1697
+ // This condition should only be triggered if waitForInitialization was called multiple times.
1698
+ if (this.initializedPromise) {
1699
+ return this.promiseWithTimeout(this.initializedPromise, timeout);
1700
+ }
1701
+ // Create a new promise for tracking initialization
1702
+ if (!this.initializedPromise) {
1703
+ this.initializedPromise = new Promise((resolve) => {
1704
+ this.initResolve = resolve;
1705
+ });
1706
+ }
1707
+ return this.promiseWithTimeout(this.initializedPromise, timeout);
1708
+ }
1709
+ /**
1710
+ * Apply a timeout promise to a base promise. This is for use with waitForInitialization.
1711
+ *
1712
+ * @param basePromise The promise to race against a timeout.
1713
+ * @param timeout The timeout in seconds.
1714
+ * @returns A promise that resolves to the initialization result or timeout.
1715
+ *
1716
+ * @privateRemarks
1717
+ * This method is protected because it is used by the browser SDK's `start` method.
1718
+ * Eventually, the start method will be moved to this common implementation and this method will
1719
+ * be made private.
1720
+ */
1721
+ promiseWithTimeout(basePromise, timeout) {
1722
+ const cancelableTimeout = jsSdkCommon.cancelableTimedPromise(timeout, 'waitForInitialization');
1723
+ return Promise.race([
1724
+ basePromise.then((res) => {
1725
+ cancelableTimeout.cancel();
1726
+ return res;
1727
+ }),
1728
+ cancelableTimeout.promise
1729
+ // If the promise resolves without error, then the initialization completed successfully.
1730
+ // NOTE: this should never return as the resolution would only be triggered by the basePromise
1731
+ // being resolved.
1732
+ .then(() => ({ status: 'complete' }))
1733
+ .catch(() => ({ status: 'timeout' })),
1734
+ ]).catch((reason) => {
1735
+ this.logger?.error(reason.message);
1736
+ return { status: 'failed', error: reason };
1737
+ });
1738
+ }
1549
1739
  on(eventName, listener) {
1550
1740
  this.emitter.on(eventName, listener);
1551
1741
  }
@@ -1553,7 +1743,7 @@ class LDClientImpl {
1553
1743
  this.emitter.off(eventName, listener);
1554
1744
  }
1555
1745
  track(key, data, metricValue) {
1556
- if (!this._checkedContext || !this._checkedContext.valid) {
1746
+ if (!this._activeContextTracker.hasValidContext()) {
1557
1747
  this.logger.warn(ClientMessages.MissingContextKeyNoEvent);
1558
1748
  return;
1559
1749
  }
@@ -1561,37 +1751,46 @@ class LDClientImpl {
1561
1751
  if (metricValue !== undefined && !jsSdkCommon.TypeValidators.Number.is(metricValue)) {
1562
1752
  this.logger?.warn(ClientMessages.invalidMetricValue(typeof metricValue));
1563
1753
  }
1564
- this._eventProcessor?.sendEvent(this._config.trackEventModifier(this._eventFactoryDefault.customEvent(key, this._checkedContext, data, metricValue)));
1754
+ this._eventProcessor?.sendEvent(this._config.trackEventModifier(this._eventFactoryDefault.customEvent(key, this._activeContextTracker.getContext(), data, metricValue)));
1565
1755
  this._hookRunner.afterTrack({
1566
1756
  key,
1567
1757
  // The context is pre-checked above, so we know it can be unwrapped.
1568
- context: this._uncheckedContext,
1758
+ context: this._activeContextTracker.getUnwrappedContext(),
1569
1759
  data,
1570
1760
  metricValue,
1571
1761
  });
1572
1762
  }
1573
1763
  _variationInternal(flagKey, defaultValue, eventFactory, typeChecker) {
1574
- if (!this._uncheckedContext) {
1575
- this.logger.debug(ClientMessages.MissingContextKeyNoEvent);
1576
- return createErrorEvaluationDetail(ErrorKinds.UserNotSpecified, defaultValue);
1577
- }
1578
- const evalContext = jsSdkCommon.Context.fromLDContext(this._uncheckedContext);
1764
+ // We are letting evaulations happen without a context. The main case for this
1765
+ // is when cached data is loaded, but the client is not fully initialized. In this
1766
+ // case, we will write out a warning for each evaluation attempt.
1767
+ // NOTE: we will be changing this behavior soon once we have a tracker on the
1768
+ // client initialization state.
1769
+ const hasContext = this._activeContextTracker.hasContext();
1770
+ if (!hasContext) {
1771
+ this.logger?.warn('Flag evaluation called before client is fully initialized, data from this evaulation could be stale.');
1772
+ }
1773
+ const evalContext = this._activeContextTracker.getContext();
1579
1774
  const foundItem = this._flagManager.get(flagKey);
1580
1775
  if (foundItem === undefined || foundItem.flag.deleted) {
1581
1776
  const defVal = defaultValue ?? null;
1582
1777
  const error = new jsSdkCommon.LDClientError(`Unknown feature flag "${flagKey}"; returning default value ${defVal}.`);
1583
- this.emitter.emit('error', this._uncheckedContext, error);
1584
- this._eventProcessor?.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defVal, evalContext));
1778
+ this.emitter.emit('error', this._activeContextTracker.getUnwrappedContext(), error);
1779
+ if (hasContext) {
1780
+ this._eventProcessor?.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defVal, evalContext));
1781
+ }
1585
1782
  return createErrorEvaluationDetail(ErrorKinds.FlagNotFound, defaultValue);
1586
1783
  }
1587
1784
  const { reason, value, variation, prerequisites } = foundItem.flag;
1588
1785
  if (typeChecker) {
1589
1786
  const [matched, type] = typeChecker(value);
1590
1787
  if (!matched) {
1591
- this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, defaultValue, // track default value on type errors
1592
- defaultValue, foundItem.flag, evalContext, reason));
1788
+ if (hasContext) {
1789
+ this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, defaultValue, // track default value on type errors
1790
+ defaultValue, foundItem.flag, evalContext, reason));
1791
+ }
1593
1792
  const error = new jsSdkCommon.LDClientError(`Wrong type "${type}" for feature flag "${flagKey}"; returning default value`);
1594
- this.emitter.emit('error', this._uncheckedContext, error);
1793
+ this.emitter.emit('error', this._activeContextTracker.getUnwrappedContext(), error);
1595
1794
  return createErrorEvaluationDetail(ErrorKinds.WrongType, defaultValue);
1596
1795
  }
1597
1796
  }
@@ -1603,18 +1802,20 @@ class LDClientImpl {
1603
1802
  prerequisites?.forEach((prereqKey) => {
1604
1803
  this._variationInternal(prereqKey, undefined, this._eventFactoryDefault);
1605
1804
  });
1606
- this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, value, defaultValue, foundItem.flag, evalContext, reason));
1805
+ if (hasContext) {
1806
+ this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, value, defaultValue, foundItem.flag, evalContext, reason));
1807
+ }
1607
1808
  return successDetail;
1608
1809
  }
1609
1810
  variation(flagKey, defaultValue) {
1610
- const { value } = this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryDefault));
1811
+ const { value } = this._hookRunner.withEvaluation(flagKey, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryDefault));
1611
1812
  return value;
1612
1813
  }
1613
1814
  variationDetail(flagKey, defaultValue) {
1614
- return this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryWithReasons));
1815
+ return this._hookRunner.withEvaluation(flagKey, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryWithReasons));
1615
1816
  }
1616
1817
  _typedEval(key, defaultValue, eventFactory, typeChecker) {
1617
- return this._hookRunner.withEvaluation(key, this._uncheckedContext, defaultValue, () => this._variationInternal(key, defaultValue, eventFactory, typeChecker));
1818
+ return this._hookRunner.withEvaluation(key, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(key, defaultValue, eventFactory, typeChecker));
1618
1819
  }
1619
1820
  boolVariation(key, defaultValue) {
1620
1821
  return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
@@ -1696,6 +1897,9 @@ class LDClientImpl {
1696
1897
  sendEvent(event) {
1697
1898
  this._eventProcessor?.sendEvent(event);
1698
1899
  }
1900
+ getDebugOverrides() {
1901
+ return this._flagManager.getDebugOverride?.();
1902
+ }
1699
1903
  _handleInspectionChanged(flagKeys, type) {
1700
1904
  if (!this._inspectorManager.hasInspectors()) {
1701
1905
  return;
@@ -1717,6 +1921,9 @@ class LDClientImpl {
1717
1921
  };
1718
1922
  }
1719
1923
  });
1924
+ // NOTE: we are not tracking "override" changes because, at the time of writing,
1925
+ // these changes are only used for debugging purposes and are not persisted. This
1926
+ // may change in the future.
1720
1927
  if (type === 'init') {
1721
1928
  this._inspectorManager.onFlagsChanged(details);
1722
1929
  }
@@ -1728,6 +1935,24 @@ class LDClientImpl {
1728
1935
  }
1729
1936
  }
1730
1937
 
1938
+ /**
1939
+ * Safe register debug override plugins.
1940
+ *
1941
+ * @param logger The logger to use for logging errors.
1942
+ * @param debugOverride The debug override to register.
1943
+ * @param plugins The plugins to register.
1944
+ */
1945
+ function safeRegisterDebugOverridePlugins(logger, debugOverride, plugins) {
1946
+ plugins.forEach((plugin) => {
1947
+ try {
1948
+ plugin.registerDebug?.(debugOverride);
1949
+ }
1950
+ catch (error) {
1951
+ logger.error(`Exception thrown registering plugin ${jsSdkCommon.internal.safeGetName(logger, plugin)}.`);
1952
+ }
1953
+ });
1954
+ }
1955
+
1731
1956
  class DataSourceEventHandler {
1732
1957
  constructor(_flagManager, _statusManager, _logger) {
1733
1958
  this._flagManager = _flagManager;
@@ -2208,6 +2433,7 @@ exports.BaseDataManager = BaseDataManager;
2208
2433
  exports.LDClientImpl = LDClientImpl;
2209
2434
  exports.Requestor = Requestor;
2210
2435
  exports.makeRequestor = makeRequestor;
2436
+ exports.safeRegisterDebugOverridePlugins = safeRegisterDebugOverridePlugins;
2211
2437
  Object.keys(jsSdkCommon).forEach(function (k) {
2212
2438
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
2213
2439
  enumerable: true,