@launchdarkly/js-client-sdk-common 1.16.0 → 1.17.1

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 (54) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/LDClientImpl.d.ts +25 -1
  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/LDWaitForInitialization.d.ts +50 -0
  11. package/dist/cjs/api/LDWaitForInitialization.d.ts.map +1 -0
  12. package/dist/cjs/api/index.d.ts +1 -0
  13. package/dist/cjs/api/index.d.ts.map +1 -1
  14. package/dist/cjs/configuration/Configuration.d.ts +1 -0
  15. package/dist/cjs/configuration/Configuration.d.ts.map +1 -1
  16. package/dist/cjs/configuration/validators.d.ts.map +1 -1
  17. package/dist/cjs/flag-manager/FlagManager.d.ts.map +1 -1
  18. package/dist/cjs/flag-manager/FlagPersistence.d.ts +1 -1
  19. package/dist/cjs/flag-manager/FlagPersistence.d.ts.map +1 -1
  20. package/dist/cjs/flag-manager/FlagStore.d.ts +15 -13
  21. package/dist/cjs/flag-manager/FlagStore.d.ts.map +1 -1
  22. package/dist/cjs/flag-manager/FlagUpdater.d.ts +33 -6
  23. package/dist/cjs/flag-manager/FlagUpdater.d.ts.map +1 -1
  24. package/dist/cjs/index.cjs +184 -94
  25. package/dist/cjs/index.cjs.map +1 -1
  26. package/dist/cjs/index.d.ts +1 -1
  27. package/dist/cjs/index.d.ts.map +1 -1
  28. package/dist/esm/LDClientImpl.d.ts +25 -1
  29. package/dist/esm/LDClientImpl.d.ts.map +1 -1
  30. package/dist/esm/LDEmitter.d.ts +1 -1
  31. package/dist/esm/LDEmitter.d.ts.map +1 -1
  32. package/dist/esm/api/LDClient.d.ts +33 -0
  33. package/dist/esm/api/LDClient.d.ts.map +1 -1
  34. package/dist/esm/api/LDOptions.d.ts +7 -0
  35. package/dist/esm/api/LDOptions.d.ts.map +1 -1
  36. package/dist/esm/api/LDWaitForInitialization.d.ts +50 -0
  37. package/dist/esm/api/LDWaitForInitialization.d.ts.map +1 -0
  38. package/dist/esm/api/index.d.ts +1 -0
  39. package/dist/esm/api/index.d.ts.map +1 -1
  40. package/dist/esm/configuration/Configuration.d.ts +1 -0
  41. package/dist/esm/configuration/Configuration.d.ts.map +1 -1
  42. package/dist/esm/configuration/validators.d.ts.map +1 -1
  43. package/dist/esm/flag-manager/FlagManager.d.ts.map +1 -1
  44. package/dist/esm/flag-manager/FlagPersistence.d.ts +1 -1
  45. package/dist/esm/flag-manager/FlagPersistence.d.ts.map +1 -1
  46. package/dist/esm/flag-manager/FlagStore.d.ts +15 -13
  47. package/dist/esm/flag-manager/FlagStore.d.ts.map +1 -1
  48. package/dist/esm/flag-manager/FlagUpdater.d.ts +33 -6
  49. package/dist/esm/flag-manager/FlagUpdater.d.ts.map +1 -1
  50. package/dist/esm/index.d.ts +1 -1
  51. package/dist/esm/index.d.ts.map +1 -1
  52. package/dist/esm/index.mjs +185 -95
  53. package/dist/esm/index.mjs.map +1 -1
  54. package/package.json +1 -1
@@ -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;
@@ -863,30 +864,30 @@ class FlagPersistence {
863
864
  }
864
865
 
865
866
  /**
866
- * In memory flag store.
867
+ * Creates the default implementation of the flag store.
867
868
  */
868
- class DefaultFlagStore {
869
- constructor() {
870
- this._flags = {};
871
- }
872
- init(newFlags) {
873
- this._flags = Object.entries(newFlags).reduce((acc, [key, flag]) => {
874
- acc[key] = flag;
875
- return acc;
876
- }, {});
877
- }
878
- insertOrUpdate(key, update) {
879
- this._flags[key] = update;
880
- }
881
- get(key) {
882
- if (Object.prototype.hasOwnProperty.call(this._flags, key)) {
883
- return this._flags[key];
884
- }
885
- return undefined;
886
- }
887
- getAll() {
888
- return this._flags;
889
- }
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
+ };
890
891
  }
891
892
 
892
893
  function calculateChangedKeys(existingObject, newObject) {
@@ -907,70 +908,66 @@ function calculateChangedKeys(existingObject, newObject) {
907
908
  return changedKeys;
908
909
  }
909
910
 
910
- /**
911
- * The flag updater handles logic required during the flag update process.
912
- * It handles versions checking to handle out of order flag updates and
913
- * also handles flag comparisons for change notification.
914
- */
915
- class FlagUpdater {
916
- constructor(flagStore, logger) {
917
- this._changeCallbacks = new Array();
918
- this._flagStore = flagStore;
919
- this._logger = logger;
920
- }
921
- handleFlagChanges(keys, type) {
922
- if (this._activeContext) {
923
- this._changeCallbacks.forEach((callback) => {
924
- try {
925
- callback(this._activeContext, keys, type);
926
- }
927
- catch (err) {
928
- /* intentionally empty */
929
- }
930
- });
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
- }
944
- }
945
- initCached(context, newFlags) {
946
- if (this._activeContext?.canonicalKey === context.canonicalKey) {
947
- return;
948
- }
949
- this.init(context, newFlags);
950
- }
951
- upsert(context, key, item) {
952
- if (this._activeContext?.canonicalKey !== context.canonicalKey) {
953
- this._logger.warn('Received an update for an inactive context.');
954
- return false;
955
- }
956
- const currentValue = this._flagStore.get(key);
957
- if (currentValue !== undefined && currentValue.version >= item.version) {
958
- // this is an out of order update that can be ignored
959
- return false;
960
- }
961
- this._flagStore.insertOrUpdate(key, item);
962
- this.handleFlagChanges([key], 'patch');
963
- return true;
964
- }
965
- on(callback) {
966
- this._changeCallbacks.push(callback);
967
- }
968
- off(callback) {
969
- const index = this._changeCallbacks.indexOf(callback);
970
- if (index > -1) {
971
- this._changeCallbacks.splice(index, 1);
972
- }
973
- }
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
+ });
927
+ }
928
+ else {
929
+ logger.warn('Received a change event without an active context. Changes will not be propagated.');
930
+ }
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
+ };
974
971
  }
975
972
 
976
973
  class DefaultFlagManager {
@@ -982,8 +979,8 @@ class DefaultFlagManager {
982
979
  * @param timeStamper exists for testing purposes
983
980
  */
984
981
  constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
985
- this._flagStore = new DefaultFlagStore();
986
- this._flagUpdater = new FlagUpdater(this._flagStore, logger);
982
+ this._flagStore = createDefaultFlagStore();
983
+ this._flagUpdater = createFlagUpdater(this._flagStore, logger);
987
984
  this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
988
985
  }
989
986
  async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
@@ -1486,6 +1483,24 @@ class LDClientImpl {
1486
1483
  if (this._inspectorManager.hasInspectors()) {
1487
1484
  this._hookRunner.addHook(getInspectorHook(this._inspectorManager));
1488
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
+ }
1489
1504
  }
1490
1505
  allFlags() {
1491
1506
  // extracting all flag values
@@ -1630,13 +1645,18 @@ class LDClientImpl {
1630
1645
  }, identifyOptions?.sheddable ?? false)
1631
1646
  .then((res) => {
1632
1647
  if (res.status === 'error') {
1633
- 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;
1634
1652
  }
1635
1653
  if (res.status === 'shed') {
1636
1654
  return { status: 'shed' };
1637
1655
  }
1638
- this.emitter.emit('initialized');
1639
- return { status: 'completed' };
1656
+ const successResult = { status: 'completed' };
1657
+ // Track initialization state for waitForInitialization
1658
+ this.maybeSetInitializationResult({ status: 'complete' });
1659
+ return successResult;
1640
1660
  });
1641
1661
  if (noTimeout) {
1642
1662
  return callSitePromise;
@@ -1648,6 +1668,74 @@ class LDClientImpl {
1648
1668
  });
1649
1669
  return Promise.race([callSitePromise, timeoutPromise]);
1650
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
+ }
1651
1739
  on(eventName, listener) {
1652
1740
  this.emitter.on(eventName, listener);
1653
1741
  }
@@ -1686,8 +1774,7 @@ class LDClientImpl {
1686
1774
  const foundItem = this._flagManager.get(flagKey);
1687
1775
  if (foundItem === undefined || foundItem.flag.deleted) {
1688
1776
  const defVal = defaultValue ?? null;
1689
- const error = new jsSdkCommon.LDClientError(`Unknown feature flag "${flagKey}"; returning default value ${defVal}.`);
1690
- this.emitter.emit('error', this._activeContextTracker.getUnwrappedContext(), error);
1777
+ this.logger?.warn(`Unknown feature flag "${flagKey}"; returning default value ${defVal}.`);
1691
1778
  if (hasContext) {
1692
1779
  this._eventProcessor?.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defVal, evalContext));
1693
1780
  }
@@ -1833,6 +1920,9 @@ class LDClientImpl {
1833
1920
  };
1834
1921
  }
1835
1922
  });
1923
+ // NOTE: we are not tracking "override" changes because, at the time of writing,
1924
+ // these changes are only used for debugging purposes and are not persisted. This
1925
+ // may change in the future.
1836
1926
  if (type === 'init') {
1837
1927
  this._inspectorManager.onFlagsChanged(details);
1838
1928
  }