@launchdarkly/js-client-sdk-common 1.15.2 → 1.16.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 (42) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/LDClientImpl.d.ts +12 -3
  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/LDPlugin.d.ts +16 -0
  7. package/dist/cjs/api/LDPlugin.d.ts.map +1 -0
  8. package/dist/cjs/api/index.d.ts +1 -0
  9. package/dist/cjs/api/index.d.ts.map +1 -1
  10. package/dist/cjs/context/createActiveContextTracker.d.ts +49 -0
  11. package/dist/cjs/context/createActiveContextTracker.d.ts.map +1 -0
  12. package/dist/cjs/flag-manager/FlagManager.d.ts +70 -1
  13. package/dist/cjs/flag-manager/FlagManager.d.ts.map +1 -1
  14. package/dist/cjs/flag-manager/FlagUpdater.d.ts +3 -2
  15. package/dist/cjs/flag-manager/FlagUpdater.d.ts.map +1 -1
  16. package/dist/cjs/index.cjs +185 -50
  17. package/dist/cjs/index.cjs.map +1 -1
  18. package/dist/cjs/index.d.ts +3 -2
  19. package/dist/cjs/index.d.ts.map +1 -1
  20. package/dist/cjs/plugins/safeRegisterDebugOverridePlugins.d.ts +12 -0
  21. package/dist/cjs/plugins/safeRegisterDebugOverridePlugins.d.ts.map +1 -0
  22. package/dist/esm/LDClientImpl.d.ts +12 -3
  23. package/dist/esm/LDClientImpl.d.ts.map +1 -1
  24. package/dist/esm/LDEmitter.d.ts +1 -1
  25. package/dist/esm/LDEmitter.d.ts.map +1 -1
  26. package/dist/esm/api/LDPlugin.d.ts +16 -0
  27. package/dist/esm/api/LDPlugin.d.ts.map +1 -0
  28. package/dist/esm/api/index.d.ts +1 -0
  29. package/dist/esm/api/index.d.ts.map +1 -1
  30. package/dist/esm/context/createActiveContextTracker.d.ts +49 -0
  31. package/dist/esm/context/createActiveContextTracker.d.ts.map +1 -0
  32. package/dist/esm/flag-manager/FlagManager.d.ts +70 -1
  33. package/dist/esm/flag-manager/FlagManager.d.ts.map +1 -1
  34. package/dist/esm/flag-manager/FlagUpdater.d.ts +3 -2
  35. package/dist/esm/flag-manager/FlagUpdater.d.ts.map +1 -1
  36. package/dist/esm/index.d.ts +3 -2
  37. package/dist/esm/index.d.ts.map +1 -1
  38. package/dist/esm/index.mjs +185 -51
  39. package/dist/esm/index.mjs.map +1 -1
  40. package/dist/esm/plugins/safeRegisterDebugOverridePlugins.d.ts +12 -0
  41. package/dist/esm/plugins/safeRegisterDebugOverridePlugins.d.ts.map +1 -0
  42. package/package.json +2 -2
@@ -538,6 +538,38 @@ const addAutoEnv = async (context, platform, config) => {
538
538
  return context;
539
539
  };
540
540
 
541
+ function createActiveContextTracker() {
542
+ let unwrappedContext;
543
+ let context;
544
+ return {
545
+ set(_unwrappedContext, _context) {
546
+ unwrappedContext = _unwrappedContext;
547
+ context = _context;
548
+ },
549
+ getContext() {
550
+ return context;
551
+ },
552
+ getUnwrappedContext() {
553
+ return unwrappedContext;
554
+ },
555
+ newIdentificationPromise() {
556
+ let res;
557
+ let rej;
558
+ const basePromise = new Promise((resolve, reject) => {
559
+ res = resolve;
560
+ rej = reject;
561
+ });
562
+ return { identifyPromise: basePromise, identifyResolve: res, identifyReject: rej };
563
+ },
564
+ hasContext() {
565
+ return context !== undefined;
566
+ },
567
+ hasValidContext() {
568
+ return this.hasContext() && context.valid;
569
+ },
570
+ };
571
+ }
572
+
541
573
  const { isLegacyUser, isMultiKind, isSingleKind } = jsSdkCommon.internal;
542
574
  /**
543
575
  * This is the root ensureKey function. All other ensureKey functions reduce to this.
@@ -886,30 +918,38 @@ class FlagUpdater {
886
918
  this._flagStore = flagStore;
887
919
  this._logger = logger;
888
920
  }
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) {
921
+ handleFlagChanges(keys, type) {
922
+ if (this._activeContext) {
895
923
  this._changeCallbacks.forEach((callback) => {
896
924
  try {
897
- callback(context, changed, 'init');
925
+ callback(this._activeContext, keys, type);
898
926
  }
899
927
  catch (err) {
900
928
  /* intentionally empty */
901
929
  }
902
930
  });
903
931
  }
932
+ else {
933
+ this._logger.warn('Received a change event without an active context. Changes will not be propagated.');
934
+ }
935
+ }
936
+ init(context, newFlags) {
937
+ this._activeContext = context;
938
+ const oldFlags = this._flagStore.getAll();
939
+ this._flagStore.init(newFlags);
940
+ const changed = calculateChangedKeys(oldFlags, newFlags);
941
+ if (changed.length > 0) {
942
+ this.handleFlagChanges(changed, 'init');
943
+ }
904
944
  }
905
945
  initCached(context, newFlags) {
906
- if (this._activeContextKey === context.canonicalKey) {
946
+ if (this._activeContext?.canonicalKey === context.canonicalKey) {
907
947
  return;
908
948
  }
909
949
  this.init(context, newFlags);
910
950
  }
911
951
  upsert(context, key, item) {
912
- if (this._activeContextKey !== context.canonicalKey) {
952
+ if (this._activeContext?.canonicalKey !== context.canonicalKey) {
913
953
  this._logger.warn('Received an update for an inactive context.');
914
954
  return false;
915
955
  }
@@ -919,14 +959,7 @@ class FlagUpdater {
919
959
  return false;
920
960
  }
921
961
  this._flagStore.insertOrUpdate(key, item);
922
- this._changeCallbacks.forEach((callback) => {
923
- try {
924
- callback(context, [key], 'patch');
925
- }
926
- catch (err) {
927
- /* intentionally empty */
928
- }
929
- });
962
+ this.handleFlagChanges([key], 'patch');
930
963
  return true;
931
964
  }
932
965
  on(callback) {
@@ -958,11 +991,26 @@ class DefaultFlagManager {
958
991
  return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
959
992
  }
960
993
  get(key) {
994
+ if (this._overrides && Object.prototype.hasOwnProperty.call(this._overrides, key)) {
995
+ return this._convertValueToOverrideDescripter(this._overrides[key]);
996
+ }
961
997
  return this._flagStore.get(key);
962
998
  }
963
999
  getAll() {
1000
+ if (this._overrides) {
1001
+ return {
1002
+ ...this._flagStore.getAll(),
1003
+ ...Object.entries(this._overrides).reduce((acc, [key, value]) => {
1004
+ acc[key] = this._convertValueToOverrideDescripter(value);
1005
+ return acc;
1006
+ }, {}),
1007
+ };
1008
+ }
964
1009
  return this._flagStore.getAll();
965
1010
  }
1011
+ presetFlags(newFlags) {
1012
+ this._flagStore.init(newFlags);
1013
+ }
966
1014
  setBootstrap(context, newFlags) {
967
1015
  // Bypasses the persistence as we do not want to put these flags into any cache.
968
1016
  // Generally speaking persistence likely *SHOULD* be disabled when using bootstrap.
@@ -983,6 +1031,58 @@ class DefaultFlagManager {
983
1031
  off(callback) {
984
1032
  this._flagUpdater.off(callback);
985
1033
  }
1034
+ _convertValueToOverrideDescripter(value) {
1035
+ return {
1036
+ flag: {
1037
+ value,
1038
+ version: 0,
1039
+ },
1040
+ version: 0,
1041
+ };
1042
+ }
1043
+ setOverride(key, value) {
1044
+ if (!this._overrides) {
1045
+ this._overrides = {};
1046
+ }
1047
+ this._overrides[key] = value;
1048
+ this._flagUpdater.handleFlagChanges([key], 'override');
1049
+ }
1050
+ removeOverride(flagKey) {
1051
+ if (!this._overrides || !Object.prototype.hasOwnProperty.call(this._overrides, flagKey)) {
1052
+ return; // No override to remove
1053
+ }
1054
+ delete this._overrides[flagKey];
1055
+ // If no more overrides, reset to undefined for performance
1056
+ if (Object.keys(this._overrides).length === 0) {
1057
+ this._overrides = undefined;
1058
+ }
1059
+ this._flagUpdater.handleFlagChanges([flagKey], 'override');
1060
+ }
1061
+ clearAllOverrides() {
1062
+ if (this._overrides) {
1063
+ const clearedOverrides = { ...this._overrides };
1064
+ this._overrides = undefined; // Reset to undefined
1065
+ this._flagUpdater.handleFlagChanges(Object.keys(clearedOverrides), 'override');
1066
+ }
1067
+ }
1068
+ getAllOverrides() {
1069
+ if (!this._overrides) {
1070
+ return {};
1071
+ }
1072
+ const result = {};
1073
+ Object.entries(this._overrides).forEach(([key, value]) => {
1074
+ result[key] = this._convertValueToOverrideDescripter(value);
1075
+ });
1076
+ return result;
1077
+ }
1078
+ getDebugOverride() {
1079
+ return {
1080
+ setOverride: this.setOverride.bind(this),
1081
+ removeOverride: this.removeOverride.bind(this),
1082
+ clearAllOverrides: this.clearAllOverrides.bind(this),
1083
+ getAllOverrides: this.getAllOverrides.bind(this),
1084
+ };
1085
+ }
986
1086
  }
987
1087
 
988
1088
  const UNKNOWN_HOOK_NAME = 'unknown hook';
@@ -1345,6 +1445,7 @@ class LDClientImpl {
1345
1445
  this.sdkKey = sdkKey;
1346
1446
  this.autoEnvAttributes = autoEnvAttributes;
1347
1447
  this.platform = platform;
1448
+ this._activeContextTracker = createActiveContextTracker();
1348
1449
  this._highTimeoutThreshold = 15;
1349
1450
  this._eventFactoryDefault = new EventFactory(false);
1350
1451
  this._eventFactoryWithReasons = new EventFactory(true);
@@ -1419,19 +1520,20 @@ class LDClientImpl {
1419
1520
  // code. We are returned the unchecked context so that if a consumer identifies with an invalid context
1420
1521
  // and then calls getContext, they get back the same context they provided, without any assertion about
1421
1522
  // validity.
1422
- return this._uncheckedContext ? jsSdkCommon.clone(this._uncheckedContext) : undefined;
1523
+ return this._activeContextTracker.hasContext()
1524
+ ? jsSdkCommon.clone(this._activeContextTracker.getUnwrappedContext())
1525
+ : undefined;
1423
1526
  }
1424
1527
  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 };
1528
+ return this._activeContextTracker.getContext();
1529
+ }
1530
+ /**
1531
+ * Preset flags are used to set the flags before the client is initialized. This is useful for
1532
+ * when client has precached flags that are ready to evaluate without full initialization.
1533
+ * @param newFlags - The flags to preset.
1534
+ */
1535
+ presetFlags(newFlags) {
1536
+ this._flagManager.presetFlags(newFlags);
1435
1537
  }
1436
1538
  /**
1437
1539
  * Identifies a context to LaunchDarkly. See {@link LDClient.identify}.
@@ -1507,11 +1609,10 @@ class LDClientImpl {
1507
1609
  this.emitter.emit('error', context, error);
1508
1610
  return Promise.reject(error);
1509
1611
  }
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)}`);
1612
+ this._activeContextTracker.set(context, checkedContext);
1613
+ this._eventProcessor?.sendEvent(this._eventFactoryDefault.identifyEvent(checkedContext));
1614
+ const { identifyPromise, identifyResolve, identifyReject } = this._activeContextTracker.newIdentificationPromise();
1615
+ this.logger.debug(`Identifying ${JSON.stringify(checkedContext)}`);
1515
1616
  await this.dataManager.identify(identifyResolve, identifyReject, checkedContext, identifyOptions);
1516
1617
  return identifyPromise;
1517
1618
  },
@@ -1534,6 +1635,7 @@ class LDClientImpl {
1534
1635
  if (res.status === 'shed') {
1535
1636
  return { status: 'shed' };
1536
1637
  }
1638
+ this.emitter.emit('initialized');
1537
1639
  return { status: 'completed' };
1538
1640
  });
1539
1641
  if (noTimeout) {
@@ -1553,7 +1655,7 @@ class LDClientImpl {
1553
1655
  this.emitter.off(eventName, listener);
1554
1656
  }
1555
1657
  track(key, data, metricValue) {
1556
- if (!this._checkedContext || !this._checkedContext.valid) {
1658
+ if (!this._activeContextTracker.hasValidContext()) {
1557
1659
  this.logger.warn(ClientMessages.MissingContextKeyNoEvent);
1558
1660
  return;
1559
1661
  }
@@ -1561,37 +1663,46 @@ class LDClientImpl {
1561
1663
  if (metricValue !== undefined && !jsSdkCommon.TypeValidators.Number.is(metricValue)) {
1562
1664
  this.logger?.warn(ClientMessages.invalidMetricValue(typeof metricValue));
1563
1665
  }
1564
- this._eventProcessor?.sendEvent(this._config.trackEventModifier(this._eventFactoryDefault.customEvent(key, this._checkedContext, data, metricValue)));
1666
+ this._eventProcessor?.sendEvent(this._config.trackEventModifier(this._eventFactoryDefault.customEvent(key, this._activeContextTracker.getContext(), data, metricValue)));
1565
1667
  this._hookRunner.afterTrack({
1566
1668
  key,
1567
1669
  // The context is pre-checked above, so we know it can be unwrapped.
1568
- context: this._uncheckedContext,
1670
+ context: this._activeContextTracker.getUnwrappedContext(),
1569
1671
  data,
1570
1672
  metricValue,
1571
1673
  });
1572
1674
  }
1573
1675
  _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);
1676
+ // We are letting evaulations happen without a context. The main case for this
1677
+ // is when cached data is loaded, but the client is not fully initialized. In this
1678
+ // case, we will write out a warning for each evaluation attempt.
1679
+ // NOTE: we will be changing this behavior soon once we have a tracker on the
1680
+ // client initialization state.
1681
+ const hasContext = this._activeContextTracker.hasContext();
1682
+ if (!hasContext) {
1683
+ this.logger?.warn('Flag evaluation called before client is fully initialized, data from this evaulation could be stale.');
1684
+ }
1685
+ const evalContext = this._activeContextTracker.getContext();
1579
1686
  const foundItem = this._flagManager.get(flagKey);
1580
1687
  if (foundItem === undefined || foundItem.flag.deleted) {
1581
1688
  const defVal = defaultValue ?? null;
1582
1689
  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));
1690
+ this.emitter.emit('error', this._activeContextTracker.getUnwrappedContext(), error);
1691
+ if (hasContext) {
1692
+ this._eventProcessor?.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defVal, evalContext));
1693
+ }
1585
1694
  return createErrorEvaluationDetail(ErrorKinds.FlagNotFound, defaultValue);
1586
1695
  }
1587
1696
  const { reason, value, variation, prerequisites } = foundItem.flag;
1588
1697
  if (typeChecker) {
1589
1698
  const [matched, type] = typeChecker(value);
1590
1699
  if (!matched) {
1591
- this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, defaultValue, // track default value on type errors
1592
- defaultValue, foundItem.flag, evalContext, reason));
1700
+ if (hasContext) {
1701
+ this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, defaultValue, // track default value on type errors
1702
+ defaultValue, foundItem.flag, evalContext, reason));
1703
+ }
1593
1704
  const error = new jsSdkCommon.LDClientError(`Wrong type "${type}" for feature flag "${flagKey}"; returning default value`);
1594
- this.emitter.emit('error', this._uncheckedContext, error);
1705
+ this.emitter.emit('error', this._activeContextTracker.getUnwrappedContext(), error);
1595
1706
  return createErrorEvaluationDetail(ErrorKinds.WrongType, defaultValue);
1596
1707
  }
1597
1708
  }
@@ -1603,18 +1714,20 @@ class LDClientImpl {
1603
1714
  prerequisites?.forEach((prereqKey) => {
1604
1715
  this._variationInternal(prereqKey, undefined, this._eventFactoryDefault);
1605
1716
  });
1606
- this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, value, defaultValue, foundItem.flag, evalContext, reason));
1717
+ if (hasContext) {
1718
+ this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, value, defaultValue, foundItem.flag, evalContext, reason));
1719
+ }
1607
1720
  return successDetail;
1608
1721
  }
1609
1722
  variation(flagKey, defaultValue) {
1610
- const { value } = this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryDefault));
1723
+ const { value } = this._hookRunner.withEvaluation(flagKey, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryDefault));
1611
1724
  return value;
1612
1725
  }
1613
1726
  variationDetail(flagKey, defaultValue) {
1614
- return this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryWithReasons));
1727
+ return this._hookRunner.withEvaluation(flagKey, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryWithReasons));
1615
1728
  }
1616
1729
  _typedEval(key, defaultValue, eventFactory, typeChecker) {
1617
- return this._hookRunner.withEvaluation(key, this._uncheckedContext, defaultValue, () => this._variationInternal(key, defaultValue, eventFactory, typeChecker));
1730
+ return this._hookRunner.withEvaluation(key, this._activeContextTracker.getUnwrappedContext(), defaultValue, () => this._variationInternal(key, defaultValue, eventFactory, typeChecker));
1618
1731
  }
1619
1732
  boolVariation(key, defaultValue) {
1620
1733
  return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
@@ -1696,6 +1809,9 @@ class LDClientImpl {
1696
1809
  sendEvent(event) {
1697
1810
  this._eventProcessor?.sendEvent(event);
1698
1811
  }
1812
+ getDebugOverrides() {
1813
+ return this._flagManager.getDebugOverride?.();
1814
+ }
1699
1815
  _handleInspectionChanged(flagKeys, type) {
1700
1816
  if (!this._inspectorManager.hasInspectors()) {
1701
1817
  return;
@@ -1728,6 +1844,24 @@ class LDClientImpl {
1728
1844
  }
1729
1845
  }
1730
1846
 
1847
+ /**
1848
+ * Safe register debug override plugins.
1849
+ *
1850
+ * @param logger The logger to use for logging errors.
1851
+ * @param debugOverride The debug override to register.
1852
+ * @param plugins The plugins to register.
1853
+ */
1854
+ function safeRegisterDebugOverridePlugins(logger, debugOverride, plugins) {
1855
+ plugins.forEach((plugin) => {
1856
+ try {
1857
+ plugin.registerDebug?.(debugOverride);
1858
+ }
1859
+ catch (error) {
1860
+ logger.error(`Exception thrown registering plugin ${jsSdkCommon.internal.safeGetName(logger, plugin)}.`);
1861
+ }
1862
+ });
1863
+ }
1864
+
1731
1865
  class DataSourceEventHandler {
1732
1866
  constructor(_flagManager, _statusManager, _logger) {
1733
1867
  this._flagManager = _flagManager;
@@ -2208,6 +2342,7 @@ exports.BaseDataManager = BaseDataManager;
2208
2342
  exports.LDClientImpl = LDClientImpl;
2209
2343
  exports.Requestor = Requestor;
2210
2344
  exports.makeRequestor = makeRequestor;
2345
+ exports.safeRegisterDebugOverridePlugins = safeRegisterDebugOverridePlugins;
2211
2346
  Object.keys(jsSdkCommon).forEach(function (k) {
2212
2347
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
2213
2348
  enumerable: true,