@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
@@ -1,4 +1,4 @@
1
- import { getPollingUri, TypeValidators, createSafeLogger, ServiceEndpoints, ApplicationTags, OptionMessages, NumberWithMinimum, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, LDTimeoutError, AutoEnvAttributes, LDClientError, isHttpRecoverable, httpErrorMessage, LDPollingError, DataSourceErrorKind, getStreamingUri, shouldRetry, LDStreamingError } from '@launchdarkly/js-sdk-common';
1
+ import { getPollingUri, TypeValidators, createSafeLogger, ServiceEndpoints, ApplicationTags, OptionMessages, NumberWithMinimum, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, LDTimeoutError, AutoEnvAttributes, cancelableTimedPromise, LDClientError, isHttpRecoverable, httpErrorMessage, LDPollingError, DataSourceErrorKind, getStreamingUri, shouldRetry, LDStreamingError } from '@launchdarkly/js-sdk-common';
2
2
  export * from '@launchdarkly/js-sdk-common';
3
3
  import * as jsSdkCommon from '@launchdarkly/js-sdk-common';
4
4
  export { jsSdkCommon as platform };
@@ -242,6 +242,7 @@ const validators = {
242
242
  payloadFilterKey: TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
243
243
  hooks: TypeValidators.createTypeArray('Hook[]', {}),
244
244
  inspectors: TypeValidators.createTypeArray('LDInspection', {}),
245
+ cleanOldPersistentData: TypeValidators.Boolean,
245
246
  };
246
247
 
247
248
  const DEFAULT_POLLING_INTERVAL = 60 * 5;
@@ -520,6 +521,38 @@ const addAutoEnv = async (context, platform, config) => {
520
521
  return context;
521
522
  };
522
523
 
524
+ function createActiveContextTracker() {
525
+ let unwrappedContext;
526
+ let context;
527
+ return {
528
+ set(_unwrappedContext, _context) {
529
+ unwrappedContext = _unwrappedContext;
530
+ context = _context;
531
+ },
532
+ getContext() {
533
+ return context;
534
+ },
535
+ getUnwrappedContext() {
536
+ return unwrappedContext;
537
+ },
538
+ newIdentificationPromise() {
539
+ let res;
540
+ let rej;
541
+ const basePromise = new Promise((resolve, reject) => {
542
+ res = resolve;
543
+ rej = reject;
544
+ });
545
+ return { identifyPromise: basePromise, identifyResolve: res, identifyReject: rej };
546
+ },
547
+ hasContext() {
548
+ return context !== undefined;
549
+ },
550
+ hasValidContext() {
551
+ return this.hasContext() && context.valid;
552
+ },
553
+ };
554
+ }
555
+
523
556
  const { isLegacyUser, isMultiKind, isSingleKind } = internal;
524
557
  /**
525
558
  * This is the root ensureKey function. All other ensureKey functions reduce to this.
@@ -813,30 +846,30 @@ class FlagPersistence {
813
846
  }
814
847
 
815
848
  /**
816
- * In memory flag store.
849
+ * Creates the default implementation of the flag store.
817
850
  */
818
- class DefaultFlagStore {
819
- constructor() {
820
- this._flags = {};
821
- }
822
- init(newFlags) {
823
- this._flags = Object.entries(newFlags).reduce((acc, [key, flag]) => {
824
- acc[key] = flag;
825
- return acc;
826
- }, {});
827
- }
828
- insertOrUpdate(key, update) {
829
- this._flags[key] = update;
830
- }
831
- get(key) {
832
- if (Object.prototype.hasOwnProperty.call(this._flags, key)) {
833
- return this._flags[key];
834
- }
835
- return undefined;
836
- }
837
- getAll() {
838
- return this._flags;
839
- }
851
+ function createDefaultFlagStore() {
852
+ let flags = {};
853
+ return {
854
+ init(newFlags) {
855
+ flags = Object.entries(newFlags).reduce((acc, [key, flag]) => {
856
+ acc[key] = flag;
857
+ return acc;
858
+ }, {});
859
+ },
860
+ insertOrUpdate(key, update) {
861
+ flags[key] = update;
862
+ },
863
+ get(key) {
864
+ if (Object.prototype.hasOwnProperty.call(flags, key)) {
865
+ return flags[key];
866
+ }
867
+ return undefined;
868
+ },
869
+ getAll() {
870
+ return flags;
871
+ },
872
+ };
840
873
  }
841
874
 
842
875
  function calculateChangedKeys(existingObject, newObject) {
@@ -857,69 +890,66 @@ function calculateChangedKeys(existingObject, newObject) {
857
890
  return changedKeys;
858
891
  }
859
892
 
860
- /**
861
- * The flag updater handles logic required during the flag update process.
862
- * It handles versions checking to handle out of order flag updates and
863
- * also handles flag comparisons for change notification.
864
- */
865
- class FlagUpdater {
866
- constructor(flagStore, logger) {
867
- this._changeCallbacks = new Array();
868
- this._flagStore = flagStore;
869
- this._logger = logger;
870
- }
871
- init(context, newFlags) {
872
- this._activeContextKey = context.canonicalKey;
873
- const oldFlags = this._flagStore.getAll();
874
- this._flagStore.init(newFlags);
875
- const changed = calculateChangedKeys(oldFlags, newFlags);
876
- if (changed.length > 0) {
877
- this._changeCallbacks.forEach((callback) => {
878
- try {
879
- callback(context, changed, 'init');
880
- }
881
- catch (err) {
882
- /* intentionally empty */
883
- }
884
- });
885
- }
886
- }
887
- initCached(context, newFlags) {
888
- if (this._activeContextKey === context.canonicalKey) {
889
- return;
890
- }
891
- this.init(context, newFlags);
892
- }
893
- upsert(context, key, item) {
894
- if (this._activeContextKey !== context.canonicalKey) {
895
- this._logger.warn('Received an update for an inactive context.');
896
- return false;
897
- }
898
- const currentValue = this._flagStore.get(key);
899
- if (currentValue !== undefined && currentValue.version >= item.version) {
900
- // this is an out of order update that can be ignored
901
- return false;
902
- }
903
- this._flagStore.insertOrUpdate(key, item);
904
- this._changeCallbacks.forEach((callback) => {
905
- try {
906
- callback(context, [key], 'patch');
893
+ function createFlagUpdater(_flagStore, _logger) {
894
+ const flagStore = _flagStore;
895
+ const logger = _logger;
896
+ let activeContext;
897
+ const changeCallbacks = new Array();
898
+ return {
899
+ handleFlagChanges(keys, type) {
900
+ if (activeContext) {
901
+ changeCallbacks.forEach((callback) => {
902
+ try {
903
+ callback(activeContext, keys, type);
904
+ }
905
+ catch (err) {
906
+ /* intentionally empty */
907
+ }
908
+ });
907
909
  }
908
- catch (err) {
909
- /* intentionally empty */
910
+ else {
911
+ logger.warn('Received a change event without an active context. Changes will not be propagated.');
910
912
  }
911
- });
912
- return true;
913
- }
914
- on(callback) {
915
- this._changeCallbacks.push(callback);
916
- }
917
- off(callback) {
918
- const index = this._changeCallbacks.indexOf(callback);
919
- if (index > -1) {
920
- this._changeCallbacks.splice(index, 1);
921
- }
922
- }
913
+ },
914
+ init(context, newFlags) {
915
+ activeContext = context;
916
+ const oldFlags = flagStore.getAll();
917
+ flagStore.init(newFlags);
918
+ const changed = calculateChangedKeys(oldFlags, newFlags);
919
+ if (changed.length > 0) {
920
+ this.handleFlagChanges(changed, 'init');
921
+ }
922
+ },
923
+ initCached(context, newFlags) {
924
+ if (activeContext?.canonicalKey === context.canonicalKey) {
925
+ return;
926
+ }
927
+ this.init(context, newFlags);
928
+ },
929
+ upsert(context, key, item) {
930
+ if (activeContext?.canonicalKey !== context.canonicalKey) {
931
+ logger.warn('Received an update for an inactive context.');
932
+ return false;
933
+ }
934
+ const currentValue = flagStore.get(key);
935
+ if (currentValue !== undefined && currentValue.version >= item.version) {
936
+ // this is an out of order update that can be ignored
937
+ return false;
938
+ }
939
+ flagStore.insertOrUpdate(key, item);
940
+ this.handleFlagChanges([key], 'patch');
941
+ return true;
942
+ },
943
+ on(callback) {
944
+ changeCallbacks.push(callback);
945
+ },
946
+ off(callback) {
947
+ const index = changeCallbacks.indexOf(callback);
948
+ if (index > -1) {
949
+ changeCallbacks.splice(index, 1);
950
+ }
951
+ },
952
+ };
923
953
  }
924
954
 
925
955
  class DefaultFlagManager {
@@ -931,8 +961,8 @@ class DefaultFlagManager {
931
961
  * @param timeStamper exists for testing purposes
932
962
  */
933
963
  constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
934
- this._flagStore = new DefaultFlagStore();
935
- this._flagUpdater = new FlagUpdater(this._flagStore, logger);
964
+ this._flagStore = createDefaultFlagStore();
965
+ this._flagUpdater = createFlagUpdater(this._flagStore, logger);
936
966
  this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
937
967
  }
938
968
  async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
@@ -940,11 +970,26 @@ class DefaultFlagManager {
940
970
  return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
941
971
  }
942
972
  get(key) {
973
+ if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
974
+ return this._convertValueToOverrideDescripter(this._overrides[key]);
975
+ }
943
976
  return this._flagStore.get(key);
944
977
  }
945
978
  getAll() {
979
+ if (this._overrides) {
980
+ return {
981
+ ...this._flagStore.getAll(),
982
+ ...Object.entries(this._overrides).reduce((acc, [key, value]) => {
983
+ acc[key] = this._convertValueToOverrideDescripter(value);
984
+ return acc;
985
+ }, {}),
986
+ };
987
+ }
946
988
  return this._flagStore.getAll();
947
989
  }
990
+ presetFlags(newFlags) {
991
+ this._flagStore.init(newFlags);
992
+ }
948
993
  setBootstrap(context, newFlags) {
949
994
  // Bypasses the persistence as we do not want to put these flags into any cache.
950
995
  // Generally speaking persistence likely *SHOULD* be disabled when using bootstrap.
@@ -965,6 +1010,58 @@ class DefaultFlagManager {
965
1010
  off(callback) {
966
1011
  this._flagUpdater.off(callback);
967
1012
  }
1013
+ _convertValueToOverrideDescripter(value) {
1014
+ return {
1015
+ flag: {
1016
+ value,
1017
+ version: 0,
1018
+ },
1019
+ version: 0,
1020
+ };
1021
+ }
1022
+ setOverride(key, value) {
1023
+ if (!this._overrides) {
1024
+ this._overrides = {};
1025
+ }
1026
+ this._overrides[key] = value;
1027
+ this._flagUpdater.handleFlagChanges([key], 'override');
1028
+ }
1029
+ removeOverride(flagKey) {
1030
+ if (!this._overrides || !Object.prototype.hasOwnProperty.call(this._overrides, flagKey)) {
1031
+ return; // No override to remove
1032
+ }
1033
+ delete this._overrides[flagKey];
1034
+ // If no more overrides, reset to undefined for performance
1035
+ if (Object.keys(this._overrides).length === 0) {
1036
+ this._overrides = undefined;
1037
+ }
1038
+ this._flagUpdater.handleFlagChanges([flagKey], 'override');
1039
+ }
1040
+ clearAllOverrides() {
1041
+ if (this._overrides) {
1042
+ const clearedOverrides = { ...this._overrides };
1043
+ this._overrides = undefined; // Reset to undefined
1044
+ this._flagUpdater.handleFlagChanges(Object.keys(clearedOverrides), 'override');
1045
+ }
1046
+ }
1047
+ getAllOverrides() {
1048
+ if (!this._overrides) {
1049
+ return {};
1050
+ }
1051
+ const result = {};
1052
+ Object.entries(this._overrides).forEach(([key, value]) => {
1053
+ result[key] = this._convertValueToOverrideDescripter(value);
1054
+ });
1055
+ return result;
1056
+ }
1057
+ getDebugOverride() {
1058
+ return {
1059
+ setOverride: this.setOverride.bind(this),
1060
+ removeOverride: this.removeOverride.bind(this),
1061
+ clearAllOverrides: this.clearAllOverrides.bind(this),
1062
+ getAllOverrides: this.getAllOverrides.bind(this),
1063
+ };
1064
+ }
968
1065
  }
969
1066
 
970
1067
  const UNKNOWN_HOOK_NAME = 'unknown hook';
@@ -1327,6 +1424,7 @@ class LDClientImpl {
1327
1424
  this.sdkKey = sdkKey;
1328
1425
  this.autoEnvAttributes = autoEnvAttributes;
1329
1426
  this.platform = platform;
1427
+ this._activeContextTracker = createActiveContextTracker();
1330
1428
  this._highTimeoutThreshold = 15;
1331
1429
  this._eventFactoryDefault = new EventFactory(false);
1332
1430
  this._eventFactoryWithReasons = new EventFactory(true);
@@ -1367,6 +1465,24 @@ class LDClientImpl {
1367
1465
  if (this._inspectorManager.hasInspectors()) {
1368
1466
  this._hookRunner.addHook(getInspectorHook(this._inspectorManager));
1369
1467
  }
1468
+ if (options.cleanOldPersistentData &&
1469
+ internalOptions?.getLegacyStorageKeys &&
1470
+ this.platform.storage) {
1471
+ // NOTE: we are letting this fail silently because it's not critical and we don't want to block the client from initializing.
1472
+ try {
1473
+ this.logger.debug('Cleaning old persistent data.');
1474
+ Promise.all(internalOptions.getLegacyStorageKeys().map((key) => this.platform.storage?.clear(key)))
1475
+ .catch((error) => {
1476
+ this.logger.error(`Error cleaning old persistent data: ${error}`);
1477
+ })
1478
+ .finally(() => {
1479
+ this.logger.debug('Cleaned old persistent data.');
1480
+ });
1481
+ }
1482
+ catch (error) {
1483
+ this.logger.error(`Error cleaning old persistent data: ${error}`);
1484
+ }
1485
+ }
1370
1486
  }
1371
1487
  allFlags() {
1372
1488
  // extracting all flag values
@@ -1401,19 +1517,20 @@ class LDClientImpl {
1401
1517
  // code. We are returned the unchecked context so that if a consumer identifies with an invalid context
1402
1518
  // and then calls getContext, they get back the same context they provided, without any assertion about
1403
1519
  // validity.
1404
- return this._uncheckedContext ? clone(this._uncheckedContext) : undefined;
1520
+ return this._activeContextTracker.hasContext()
1521
+ ? clone(this._activeContextTracker.getUnwrappedContext())
1522
+ : undefined;
1405
1523
  }
1406
1524
  getInternalContext() {
1407
- return this._checkedContext;
1408
- }
1409
- _createIdentifyPromise() {
1410
- let res;
1411
- let rej;
1412
- const basePromise = new Promise((resolve, reject) => {
1413
- res = resolve;
1414
- rej = reject;
1415
- });
1416
- return { identifyPromise: basePromise, identifyResolve: res, identifyReject: rej };
1525
+ return this._activeContextTracker.getContext();
1526
+ }
1527
+ /**
1528
+ * Preset flags are used to set the flags before the client is initialized. This is useful for
1529
+ * when client has precached flags that are ready to evaluate without full initialization.
1530
+ * @param newFlags - The flags to preset.
1531
+ */
1532
+ presetFlags(newFlags) {
1533
+ this._flagManager.presetFlags(newFlags);
1417
1534
  }
1418
1535
  /**
1419
1536
  * Identifies a context to LaunchDarkly. See {@link LDClient.identify}.
@@ -1489,11 +1606,10 @@ class LDClientImpl {
1489
1606
  this.emitter.emit('error', context, error);
1490
1607
  return Promise.reject(error);
1491
1608
  }
1492
- this._uncheckedContext = context;
1493
- this._checkedContext = checkedContext;
1494
- this._eventProcessor?.sendEvent(this._eventFactoryDefault.identifyEvent(this._checkedContext));
1495
- const { identifyPromise, identifyResolve, identifyReject } = this._createIdentifyPromise();
1496
- this.logger.debug(`Identifying ${JSON.stringify(this._checkedContext)}`);
1609
+ this._activeContextTracker.set(context, checkedContext);
1610
+ this._eventProcessor?.sendEvent(this._eventFactoryDefault.identifyEvent(checkedContext));
1611
+ const { identifyPromise, identifyResolve, identifyReject } = this._activeContextTracker.newIdentificationPromise();
1612
+ this.logger.debug(`Identifying ${JSON.stringify(checkedContext)}`);
1497
1613
  await this.dataManager.identify(identifyResolve, identifyReject, checkedContext, identifyOptions);
1498
1614
  return identifyPromise;
1499
1615
  },
@@ -1511,12 +1627,18 @@ class LDClientImpl {
1511
1627
  }, identifyOptions?.sheddable ?? false)
1512
1628
  .then((res) => {
1513
1629
  if (res.status === 'error') {
1514
- return { status: 'error', error: res.error };
1630
+ const errorResult = { status: 'error', error: res.error };
1631
+ // Track initialization state for waitForInitialization
1632
+ this.maybeSetInitializationResult({ status: 'failed', error: res.error });
1633
+ return errorResult;
1515
1634
  }
1516
1635
  if (res.status === 'shed') {
1517
1636
  return { status: 'shed' };
1518
1637
  }
1519
- return { status: 'completed' };
1638
+ const successResult = { status: 'completed' };
1639
+ // Track initialization state for waitForInitialization
1640
+ this.maybeSetInitializationResult({ status: 'complete' });
1641
+ return successResult;
1520
1642
  });
1521
1643
  if (noTimeout) {
1522
1644
  return callSitePromise;
@@ -1528,6 +1650,74 @@ class LDClientImpl {
1528
1650
  });
1529
1651
  return Promise.race([callSitePromise, timeoutPromise]);
1530
1652
  }
1653
+ /**
1654
+ * Sets the initialization result and resolves any pending waitForInitialization promises.
1655
+ * This method is idempotent and will only be set by the initialization flow. Subsequent calls
1656
+ * should not do anything.
1657
+ * @param result The initialization result.
1658
+ */
1659
+ maybeSetInitializationResult(result) {
1660
+ if (this.initializeResult === undefined) {
1661
+ this.initializeResult = result;
1662
+ this.emitter.emit('ready');
1663
+ if (result.status === 'complete') {
1664
+ this.emitter.emit('initialized');
1665
+ }
1666
+ if (this.initResolve) {
1667
+ this.initResolve(result);
1668
+ this.initResolve = undefined;
1669
+ }
1670
+ }
1671
+ }
1672
+ waitForInitialization(options) {
1673
+ const timeout = options?.timeout ?? 5;
1674
+ // If initialization has already completed (successfully or failed), return the result immediately.
1675
+ if (this.initializeResult) {
1676
+ return Promise.resolve(this.initializeResult);
1677
+ }
1678
+ // If waitForInitialization was previously called, then return the promise with a timeout.
1679
+ // This condition should only be triggered if waitForInitialization was called multiple times.
1680
+ if (this.initializedPromise) {
1681
+ return this.promiseWithTimeout(this.initializedPromise, timeout);
1682
+ }
1683
+ // Create a new promise for tracking initialization
1684
+ if (!this.initializedPromise) {
1685
+ this.initializedPromise = new Promise((resolve) => {
1686
+ this.initResolve = resolve;
1687
+ });
1688
+ }
1689
+ return this.promiseWithTimeout(this.initializedPromise, timeout);
1690
+ }
1691
+ /**
1692
+ * Apply a timeout promise to a base promise. This is for use with waitForInitialization.
1693
+ *
1694
+ * @param basePromise The promise to race against a timeout.
1695
+ * @param timeout The timeout in seconds.
1696
+ * @returns A promise that resolves to the initialization result or timeout.
1697
+ *
1698
+ * @privateRemarks
1699
+ * This method is protected because it is used by the browser SDK's `start` method.
1700
+ * Eventually, the start method will be moved to this common implementation and this method will
1701
+ * be made private.
1702
+ */
1703
+ promiseWithTimeout(basePromise, timeout) {
1704
+ const cancelableTimeout = cancelableTimedPromise(timeout, 'waitForInitialization');
1705
+ return Promise.race([
1706
+ basePromise.then((res) => {
1707
+ cancelableTimeout.cancel();
1708
+ return res;
1709
+ }),
1710
+ cancelableTimeout.promise
1711
+ // If the promise resolves without error, then the initialization completed successfully.
1712
+ // NOTE: this should never return as the resolution would only be triggered by the basePromise
1713
+ // being resolved.
1714
+ .then(() => ({ status: 'complete' }))
1715
+ .catch(() => ({ status: 'timeout' })),
1716
+ ]).catch((reason) => {
1717
+ this.logger?.error(reason.message);
1718
+ return { status: 'failed', error: reason };
1719
+ });
1720
+ }
1531
1721
  on(eventName, listener) {
1532
1722
  this.emitter.on(eventName, listener);
1533
1723
  }
@@ -1535,7 +1725,7 @@ class LDClientImpl {
1535
1725
  this.emitter.off(eventName, listener);
1536
1726
  }
1537
1727
  track(key, data, metricValue) {
1538
- if (!this._checkedContext || !this._checkedContext.valid) {
1728
+ if (!this._activeContextTracker.hasValidContext()) {
1539
1729
  this.logger.warn(ClientMessages.MissingContextKeyNoEvent);
1540
1730
  return;
1541
1731
  }
@@ -1543,37 +1733,46 @@ class LDClientImpl {
1543
1733
  if (metricValue !== undefined && !TypeValidators.Number.is(metricValue)) {
1544
1734
  this.logger?.warn(ClientMessages.invalidMetricValue(typeof metricValue));
1545
1735
  }
1546
- this._eventProcessor?.sendEvent(this._config.trackEventModifier(this._eventFactoryDefault.customEvent(key, this._checkedContext, data, metricValue)));
1736
+ this._eventProcessor?.sendEvent(this._config.trackEventModifier(this._eventFactoryDefault.customEvent(key, this._activeContextTracker.getContext(), data, metricValue)));
1547
1737
  this._hookRunner.afterTrack({
1548
1738
  key,
1549
1739
  // The context is pre-checked above, so we know it can be unwrapped.
1550
- context: this._uncheckedContext,
1740
+ context: this._activeContextTracker.getUnwrappedContext(),
1551
1741
  data,
1552
1742
  metricValue,
1553
1743
  });
1554
1744
  }
1555
1745
  _variationInternal(flagKey, defaultValue, eventFactory, typeChecker) {
1556
- if (!this._uncheckedContext) {
1557
- this.logger.debug(ClientMessages.MissingContextKeyNoEvent);
1558
- return createErrorEvaluationDetail(ErrorKinds.UserNotSpecified, defaultValue);
1559
- }
1560
- const evalContext = Context.fromLDContext(this._uncheckedContext);
1746
+ // We are letting evaulations happen without a context. The main case for this
1747
+ // is when cached data is loaded, but the client is not fully initialized. In this
1748
+ // case, we will write out a warning for each evaluation attempt.
1749
+ // NOTE: we will be changing this behavior soon once we have a tracker on the
1750
+ // client initialization state.
1751
+ const hasContext = this._activeContextTracker.hasContext();
1752
+ if (!hasContext) {
1753
+ this.logger?.warn('Flag evaluation called before client is fully initialized, data from this evaulation could be stale.');
1754
+ }
1755
+ const evalContext = this._activeContextTracker.getContext();
1561
1756
  const foundItem = this._flagManager.get(flagKey);
1562
1757
  if (foundItem === undefined || foundItem.flag.deleted) {
1563
1758
  const defVal = defaultValue ?? null;
1564
1759
  const error = new LDClientError(`Unknown feature flag "${flagKey}"; returning default value ${defVal}.`);
1565
- this.emitter.emit('error', this._uncheckedContext, error);
1566
- this._eventProcessor?.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defVal, evalContext));
1760
+ this.emitter.emit('error', this._activeContextTracker.getUnwrappedContext(), error);
1761
+ if (hasContext) {
1762
+ this._eventProcessor?.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defVal, evalContext));
1763
+ }
1567
1764
  return createErrorEvaluationDetail(ErrorKinds.FlagNotFound, defaultValue);
1568
1765
  }
1569
1766
  const { reason, value, variation, prerequisites } = foundItem.flag;
1570
1767
  if (typeChecker) {
1571
1768
  const [matched, type] = typeChecker(value);
1572
1769
  if (!matched) {
1573
- this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, defaultValue, // track default value on type errors
1574
- defaultValue, foundItem.flag, evalContext, reason));
1770
+ if (hasContext) {
1771
+ this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, defaultValue, // track default value on type errors
1772
+ defaultValue, foundItem.flag, evalContext, reason));
1773
+ }
1575
1774
  const error = new LDClientError(`Wrong type "${type}" for feature flag "${flagKey}"; returning default value`);
1576
- this.emitter.emit('error', this._uncheckedContext, error);
1775
+ this.emitter.emit('error', this._activeContextTracker.getUnwrappedContext(), error);
1577
1776
  return createErrorEvaluationDetail(ErrorKinds.WrongType, defaultValue);
1578
1777
  }
1579
1778
  }
@@ -1585,18 +1784,20 @@ class LDClientImpl {
1585
1784
  prerequisites?.forEach((prereqKey) => {
1586
1785
  this._variationInternal(prereqKey, undefined, this._eventFactoryDefault);
1587
1786
  });
1588
- this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, value, defaultValue, foundItem.flag, evalContext, reason));
1787
+ if (hasContext) {
1788
+ this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, value, defaultValue, foundItem.flag, evalContext, reason));
1789
+ }
1589
1790
  return successDetail;
1590
1791
  }
1591
1792
  variation(flagKey, defaultValue) {
1592
- const { value } = this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryDefault));
1793
+ const { value } = this._hookRunner.withEvaluation(flagKey, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryDefault));
1593
1794
  return value;
1594
1795
  }
1595
1796
  variationDetail(flagKey, defaultValue) {
1596
- return this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryWithReasons));
1797
+ return this._hookRunner.withEvaluation(flagKey, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryWithReasons));
1597
1798
  }
1598
1799
  _typedEval(key, defaultValue, eventFactory, typeChecker) {
1599
- return this._hookRunner.withEvaluation(key, this._uncheckedContext, defaultValue, () => this._variationInternal(key, defaultValue, eventFactory, typeChecker));
1800
+ return this._hookRunner.withEvaluation(key, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(key, defaultValue, eventFactory, typeChecker));
1600
1801
  }
1601
1802
  boolVariation(key, defaultValue) {
1602
1803
  return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
@@ -1678,6 +1879,9 @@ class LDClientImpl {
1678
1879
  sendEvent(event) {
1679
1880
  this._eventProcessor?.sendEvent(event);
1680
1881
  }
1882
+ getDebugOverrides() {
1883
+ return this._flagManager.getDebugOverride?.();
1884
+ }
1681
1885
  _handleInspectionChanged(flagKeys, type) {
1682
1886
  if (!this._inspectorManager.hasInspectors()) {
1683
1887
  return;
@@ -1699,6 +1903,9 @@ class LDClientImpl {
1699
1903
  };
1700
1904
  }
1701
1905
  });
1906
+ // NOTE: we are not tracking "override" changes because, at the time of writing,
1907
+ // these changes are only used for debugging purposes and are not persisted. This
1908
+ // may change in the future.
1702
1909
  if (type === 'init') {
1703
1910
  this._inspectorManager.onFlagsChanged(details);
1704
1911
  }
@@ -1710,6 +1917,24 @@ class LDClientImpl {
1710
1917
  }
1711
1918
  }
1712
1919
 
1920
+ /**
1921
+ * Safe register debug override plugins.
1922
+ *
1923
+ * @param logger The logger to use for logging errors.
1924
+ * @param debugOverride The debug override to register.
1925
+ * @param plugins The plugins to register.
1926
+ */
1927
+ function safeRegisterDebugOverridePlugins(logger, debugOverride, plugins) {
1928
+ plugins.forEach((plugin) => {
1929
+ try {
1930
+ plugin.registerDebug?.(debugOverride);
1931
+ }
1932
+ catch (error) {
1933
+ logger.error(`Exception thrown registering plugin ${internal.safeGetName(logger, plugin)}.`);
1934
+ }
1935
+ });
1936
+ }
1937
+
1713
1938
  class DataSourceEventHandler {
1714
1939
  constructor(_flagManager, _statusManager, _logger) {
1715
1940
  this._flagManager = _flagManager;
@@ -2185,5 +2410,5 @@ class BaseDataManager {
2185
2410
  }
2186
2411
  }
2187
2412
 
2188
- export { BaseDataManager, DataSourceState, LDClientImpl, Requestor, makeRequestor };
2413
+ export { BaseDataManager, DataSourceState, LDClientImpl, Requestor, makeRequestor, safeRegisterDebugOverridePlugins };
2189
2414
  //# sourceMappingURL=index.mjs.map