@launchdarkly/js-sdk-common 2.16.0 → 2.18.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 (116) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/AttributeReference.d.ts +1 -0
  3. package/dist/cjs/AttributeReference.d.ts.map +1 -1
  4. package/dist/cjs/Context.d.ts +9 -0
  5. package/dist/cjs/Context.d.ts.map +1 -1
  6. package/dist/cjs/api/index.d.ts +1 -0
  7. package/dist/cjs/api/index.d.ts.map +1 -1
  8. package/dist/cjs/api/integrations/index.d.ts +2 -0
  9. package/dist/cjs/api/integrations/index.d.ts.map +1 -0
  10. package/dist/cjs/api/integrations/plugins.d.ts +128 -0
  11. package/dist/cjs/api/integrations/plugins.d.ts.map +1 -0
  12. package/dist/cjs/api/subsystem/DataSystem/CallbackHandler.d.ts +16 -0
  13. package/dist/cjs/api/subsystem/DataSystem/CallbackHandler.d.ts.map +1 -0
  14. package/dist/cjs/api/subsystem/DataSystem/DataSource.d.ts +21 -0
  15. package/dist/cjs/api/subsystem/DataSystem/DataSource.d.ts.map +1 -0
  16. package/dist/cjs/api/subsystem/DataSystem/index.d.ts +2 -0
  17. package/dist/cjs/api/subsystem/DataSystem/index.d.ts.map +1 -0
  18. package/dist/cjs/api/subsystem/index.d.ts +2 -1
  19. package/dist/cjs/api/subsystem/index.d.ts.map +1 -1
  20. package/dist/cjs/datasource/Backoff.d.ts +46 -0
  21. package/dist/cjs/datasource/Backoff.d.ts.map +1 -0
  22. package/dist/cjs/datasource/CompositeDataSource.d.ts +62 -0
  23. package/dist/cjs/datasource/CompositeDataSource.d.ts.map +1 -0
  24. package/dist/cjs/datasource/dataSourceList.d.ts +47 -0
  25. package/dist/cjs/datasource/dataSourceList.d.ts.map +1 -0
  26. package/dist/cjs/datasource/index.d.ts +3 -1
  27. package/dist/cjs/datasource/index.d.ts.map +1 -1
  28. package/dist/cjs/index.cjs +777 -47
  29. package/dist/cjs/index.cjs.map +1 -1
  30. package/dist/cjs/index.d.ts +2 -2
  31. package/dist/cjs/index.d.ts.map +1 -1
  32. package/dist/cjs/internal/events/EventProcessor.d.ts +1 -1
  33. package/dist/cjs/internal/events/EventProcessor.d.ts.map +1 -1
  34. package/dist/cjs/internal/events/LDEventSummarizer.d.ts +32 -0
  35. package/dist/cjs/internal/events/LDEventSummarizer.d.ts.map +1 -0
  36. package/dist/cjs/internal/events/MultiEventSummarizer.d.ts +13 -0
  37. package/dist/cjs/internal/events/MultiEventSummarizer.d.ts.map +1 -0
  38. package/dist/cjs/internal/fdv2/index.d.ts +3 -2
  39. package/dist/cjs/internal/fdv2/index.d.ts.map +1 -1
  40. package/dist/cjs/internal/fdv2/{payloadReader.d.ts → payloadProcessor.d.ts} +27 -16
  41. package/dist/cjs/internal/fdv2/payloadProcessor.d.ts.map +1 -0
  42. package/dist/cjs/internal/fdv2/payloadStreamReader.d.ts +31 -0
  43. package/dist/cjs/internal/fdv2/payloadStreamReader.d.ts.map +1 -0
  44. package/dist/cjs/internal/index.d.ts +2 -0
  45. package/dist/cjs/internal/index.d.ts.map +1 -1
  46. package/dist/cjs/internal/json/canonicalize.d.ts +10 -0
  47. package/dist/cjs/internal/json/canonicalize.d.ts.map +1 -0
  48. package/dist/cjs/internal/json/index.d.ts +2 -0
  49. package/dist/cjs/internal/json/index.d.ts.map +1 -0
  50. package/dist/cjs/internal/plugins/index.d.ts +4 -0
  51. package/dist/cjs/internal/plugins/index.d.ts.map +1 -0
  52. package/dist/cjs/internal/plugins/safeGetHooks.d.ts +4 -0
  53. package/dist/cjs/internal/plugins/safeGetHooks.d.ts.map +1 -0
  54. package/dist/cjs/internal/plugins/safeGetName.d.ts +4 -0
  55. package/dist/cjs/internal/plugins/safeGetName.d.ts.map +1 -0
  56. package/dist/cjs/internal/plugins/safeRegisterPlugins.d.ts +4 -0
  57. package/dist/cjs/internal/plugins/safeRegisterPlugins.d.ts.map +1 -0
  58. package/dist/esm/AttributeReference.d.ts +1 -0
  59. package/dist/esm/AttributeReference.d.ts.map +1 -1
  60. package/dist/esm/Context.d.ts +9 -0
  61. package/dist/esm/Context.d.ts.map +1 -1
  62. package/dist/esm/api/index.d.ts +1 -0
  63. package/dist/esm/api/index.d.ts.map +1 -1
  64. package/dist/esm/api/integrations/index.d.ts +2 -0
  65. package/dist/esm/api/integrations/index.d.ts.map +1 -0
  66. package/dist/esm/api/integrations/plugins.d.ts +128 -0
  67. package/dist/esm/api/integrations/plugins.d.ts.map +1 -0
  68. package/dist/esm/api/subsystem/DataSystem/CallbackHandler.d.ts +16 -0
  69. package/dist/esm/api/subsystem/DataSystem/CallbackHandler.d.ts.map +1 -0
  70. package/dist/esm/api/subsystem/DataSystem/DataSource.d.ts +21 -0
  71. package/dist/esm/api/subsystem/DataSystem/DataSource.d.ts.map +1 -0
  72. package/dist/esm/api/subsystem/DataSystem/index.d.ts +2 -0
  73. package/dist/esm/api/subsystem/DataSystem/index.d.ts.map +1 -0
  74. package/dist/esm/api/subsystem/index.d.ts +2 -1
  75. package/dist/esm/api/subsystem/index.d.ts.map +1 -1
  76. package/dist/esm/datasource/Backoff.d.ts +46 -0
  77. package/dist/esm/datasource/Backoff.d.ts.map +1 -0
  78. package/dist/esm/datasource/CompositeDataSource.d.ts +62 -0
  79. package/dist/esm/datasource/CompositeDataSource.d.ts.map +1 -0
  80. package/dist/esm/datasource/dataSourceList.d.ts +47 -0
  81. package/dist/esm/datasource/dataSourceList.d.ts.map +1 -0
  82. package/dist/esm/datasource/index.d.ts +3 -1
  83. package/dist/esm/datasource/index.d.ts.map +1 -1
  84. package/dist/esm/index.d.ts +2 -2
  85. package/dist/esm/index.d.ts.map +1 -1
  86. package/dist/esm/index.mjs +776 -48
  87. package/dist/esm/index.mjs.map +1 -1
  88. package/dist/esm/internal/events/EventProcessor.d.ts +1 -1
  89. package/dist/esm/internal/events/EventProcessor.d.ts.map +1 -1
  90. package/dist/esm/internal/events/LDEventSummarizer.d.ts +32 -0
  91. package/dist/esm/internal/events/LDEventSummarizer.d.ts.map +1 -0
  92. package/dist/esm/internal/events/MultiEventSummarizer.d.ts +13 -0
  93. package/dist/esm/internal/events/MultiEventSummarizer.d.ts.map +1 -0
  94. package/dist/esm/internal/fdv2/index.d.ts +3 -2
  95. package/dist/esm/internal/fdv2/index.d.ts.map +1 -1
  96. package/dist/esm/internal/fdv2/{payloadReader.d.ts → payloadProcessor.d.ts} +27 -16
  97. package/dist/esm/internal/fdv2/payloadProcessor.d.ts.map +1 -0
  98. package/dist/esm/internal/fdv2/payloadStreamReader.d.ts +31 -0
  99. package/dist/esm/internal/fdv2/payloadStreamReader.d.ts.map +1 -0
  100. package/dist/esm/internal/index.d.ts +2 -0
  101. package/dist/esm/internal/index.d.ts.map +1 -1
  102. package/dist/esm/internal/json/canonicalize.d.ts +10 -0
  103. package/dist/esm/internal/json/canonicalize.d.ts.map +1 -0
  104. package/dist/esm/internal/json/index.d.ts +2 -0
  105. package/dist/esm/internal/json/index.d.ts.map +1 -0
  106. package/dist/esm/internal/plugins/index.d.ts +4 -0
  107. package/dist/esm/internal/plugins/index.d.ts.map +1 -0
  108. package/dist/esm/internal/plugins/safeGetHooks.d.ts +4 -0
  109. package/dist/esm/internal/plugins/safeGetHooks.d.ts.map +1 -0
  110. package/dist/esm/internal/plugins/safeGetName.d.ts +4 -0
  111. package/dist/esm/internal/plugins/safeGetName.d.ts.map +1 -0
  112. package/dist/esm/internal/plugins/safeRegisterPlugins.d.ts +4 -0
  113. package/dist/esm/internal/plugins/safeRegisterPlugins.d.ts.map +1 -0
  114. package/package.json +1 -1
  115. package/dist/cjs/internal/fdv2/payloadReader.d.ts.map +0 -1
  116. package/dist/esm/internal/fdv2/payloadReader.d.ts.map +0 -1
@@ -115,6 +115,9 @@ class AttributeReference {
115
115
  return (this.depth === other.depth &&
116
116
  this._components.every((value, index) => value === other.getComponent(index)));
117
117
  }
118
+ get components() {
119
+ return [...this._components];
120
+ }
118
121
  }
119
122
  /**
120
123
  * For use as invalid references when deserializing Flag/Segment data.
@@ -314,6 +317,41 @@ function isLegacyUser(context) {
314
317
  return !('kind' in context) || context.kind === null || context.kind === undefined;
315
318
  }
316
319
 
320
+ /**
321
+ * Given some object to serialize product a canonicalized JSON string.
322
+ * https://www.rfc-editor.org/rfc/rfc8785.html
323
+ *
324
+ * We do not support custom toJSON methods on objects. Objects should be limited to basic types.
325
+ *
326
+ * @param object The object to serialize.
327
+ */
328
+ function canonicalize(object, visited = []) {
329
+ // For JavaScript the default JSON serialization will produce canonicalized output for basic types.
330
+ if (object === null || typeof object !== 'object') {
331
+ return JSON.stringify(object);
332
+ }
333
+ if (visited.includes(object)) {
334
+ throw new Error('Cycle detected');
335
+ }
336
+ if (Array.isArray(object)) {
337
+ const values = object
338
+ .map((item) => canonicalize(item, [...visited, object]))
339
+ .map((item) => (item === undefined ? 'null' : item));
340
+ return `[${values.join(',')}]`;
341
+ }
342
+ const values = Object.keys(object)
343
+ .sort()
344
+ .map((key) => {
345
+ const value = canonicalize(object[key], [...visited, object]);
346
+ if (value !== undefined) {
347
+ return `${JSON.stringify(key)}:${value}`;
348
+ }
349
+ return undefined;
350
+ })
351
+ .filter((item) => item !== undefined);
352
+ return `{${values.join(',')}}`;
353
+ }
354
+
317
355
  // The general strategy for the context is to transform the passed in context
318
356
  // as little as possible. We do convert the legacy users to a single kind
319
357
  // context, but we do not translate all passed contexts into a rigid structure.
@@ -688,6 +726,28 @@ class Context {
688
726
  get legacy() {
689
727
  return this._wasLegacy;
690
728
  }
729
+ /**
730
+ * Get the serialized canonical JSON for this context. This is not filtered for use in events.
731
+ *
732
+ * This method will cache the result.
733
+ *
734
+ * @returns The serialized canonical JSON or undefined if it cannot be serialized.
735
+ */
736
+ canonicalUnfilteredJson() {
737
+ if (!this.valid) {
738
+ return undefined;
739
+ }
740
+ if (this._cachedCanonicalJson) {
741
+ return this._cachedCanonicalJson;
742
+ }
743
+ try {
744
+ this._cachedCanonicalJson = canonicalize(Context.toLDContext(this));
745
+ }
746
+ catch {
747
+ // Indicated by undefined being returned.
748
+ }
749
+ return this._cachedCanonicalJson;
750
+ }
691
751
  }
692
752
  Context.UserKind = DEFAULT_KIND;
693
753
 
@@ -811,6 +871,482 @@ class ContextFilter {
811
871
  }
812
872
  }
813
873
 
874
+ const MAX_RETRY_DELAY = 30 * 1000; // Maximum retry delay 30 seconds.
875
+ const JITTER_RATIO = 0.5; // Delay should be 50%-100% of calculated time.
876
+ /**
877
+ * Implements exponential backoff and jitter. This class tracks successful connections and failures
878
+ * and produces a retry delay.
879
+ *
880
+ * It does not start any timers or directly control a connection.
881
+ *
882
+ * The backoff follows an exponential backoff scheme with 50% jitter starting at
883
+ * initialRetryDelayMillis and capping at MAX_RETRY_DELAY. If RESET_INTERVAL has elapsed after a
884
+ * success, without an intervening faulure, then the backoff is reset to initialRetryDelayMillis.
885
+ */
886
+ class DefaultBackoff {
887
+ constructor(initialRetryDelayMillis, _retryResetIntervalMillis, _random = Math.random) {
888
+ this._retryResetIntervalMillis = _retryResetIntervalMillis;
889
+ this._random = _random;
890
+ this._retryCount = 0;
891
+ // Initial retry delay cannot be 0.
892
+ this._initialRetryDelayMillis = Math.max(1, initialRetryDelayMillis);
893
+ this._maxExponent = Math.ceil(Math.log2(MAX_RETRY_DELAY / this._initialRetryDelayMillis));
894
+ }
895
+ _backoff() {
896
+ const exponent = Math.min(this._retryCount, this._maxExponent);
897
+ const delay = this._initialRetryDelayMillis * 2 ** exponent;
898
+ return Math.min(delay, MAX_RETRY_DELAY);
899
+ }
900
+ _jitter(computedDelayMillis) {
901
+ return computedDelayMillis - Math.trunc(this._random() * JITTER_RATIO * computedDelayMillis);
902
+ }
903
+ /**
904
+ * This function should be called when a connection attempt is successful.
905
+ *
906
+ * @param timeStampMs The time of the success. Used primarily for testing, when not provided
907
+ * the current time is used.
908
+ */
909
+ success(timeStampMs = Date.now()) {
910
+ this._activeSince = timeStampMs;
911
+ }
912
+ /**
913
+ * This function should be called when a connection fails. It returns the a delay, in
914
+ * milliseconds, after which a reconnection attempt should be made.
915
+ *
916
+ * @param timeStampMs The time of the success. Used primarily for testing, when not provided
917
+ * the current time is used.
918
+ * @returns The delay before the next connection attempt.
919
+ */
920
+ fail(timeStampMs = Date.now()) {
921
+ // If the last successful connection was active for more than the RESET_INTERVAL, then we
922
+ // return to the initial retry delay.
923
+ if (this._activeSince !== undefined &&
924
+ timeStampMs - this._activeSince > this._retryResetIntervalMillis) {
925
+ this._retryCount = 0;
926
+ }
927
+ this._activeSince = undefined;
928
+ const delay = this._jitter(this._backoff());
929
+ this._retryCount += 1;
930
+ return delay;
931
+ }
932
+ }
933
+
934
+ /**
935
+ * Handler that connects the current {@link DataSource} to the {@link CompositeDataSource}. A single
936
+ * {@link CallbackHandler} should only be given to one {@link DataSource}. Use {@link disable()} to
937
+ * prevent additional callback events.
938
+ */
939
+ class CallbackHandler {
940
+ constructor(_dataCallback, _statusCallback) {
941
+ this._dataCallback = _dataCallback;
942
+ this._statusCallback = _statusCallback;
943
+ this._disabled = false;
944
+ }
945
+ disable() {
946
+ this._disabled = true;
947
+ }
948
+ async dataHandler(basis, data) {
949
+ if (this._disabled) {
950
+ return;
951
+ }
952
+ // TODO: SDK-1044 track selector for future synchronizer to use
953
+ // report data up
954
+ this._dataCallback(basis, data);
955
+ }
956
+ async statusHandler(status, err) {
957
+ if (this._disabled) {
958
+ return;
959
+ }
960
+ this._statusCallback(status, err);
961
+ }
962
+ }
963
+
964
+ // TODO: refactor client-sdk to use this enum
965
+ var DataSourceState;
966
+ (function (DataSourceState) {
967
+ // Positive confirmation of connection/data receipt
968
+ DataSourceState[DataSourceState["Valid"] = 0] = "Valid";
969
+ // Spinning up to make first connection attempt
970
+ DataSourceState[DataSourceState["Initializing"] = 1] = "Initializing";
971
+ // Transient issue, automatic retry is expected
972
+ DataSourceState[DataSourceState["Interrupted"] = 2] = "Interrupted";
973
+ // Data source was closed and will not retry automatically.
974
+ DataSourceState[DataSourceState["Closed"] = 3] = "Closed";
975
+ })(DataSourceState || (DataSourceState = {}));
976
+
977
+ /**
978
+ * Helper class for {@link CompositeDataSource} to manage iterating on data sources and removing them on the fly.
979
+ */
980
+ class DataSourceList {
981
+ /**
982
+ * @param circular whether to loop off the end of the list back to the start
983
+ * @param initialList of content
984
+ */
985
+ constructor(circular, initialList) {
986
+ this._list = initialList ? [...initialList] : [];
987
+ this._circular = circular;
988
+ this._pos = 0;
989
+ }
990
+ /**
991
+ * Returns the current head and then iterates.
992
+ */
993
+ next() {
994
+ if (this._list.length <= 0 || this._pos >= this._list.length) {
995
+ return undefined;
996
+ }
997
+ const result = this._list[this._pos];
998
+ if (this._circular) {
999
+ this._pos = (this._pos + 1) % this._list.length;
1000
+ }
1001
+ else {
1002
+ this._pos += 1;
1003
+ }
1004
+ return result;
1005
+ }
1006
+ /**
1007
+ * Replaces all elements with the provided list and resets the position of head to the start.
1008
+ *
1009
+ * @param input that will replace existing list
1010
+ */
1011
+ replace(input) {
1012
+ this._list = [...input];
1013
+ this._pos = 0;
1014
+ }
1015
+ /**
1016
+ * Removes the provided element from the list. If the removed element was the head, head moves to next. Consider head may be undefined if list is empty after removal.
1017
+ *
1018
+ * @param element to remove
1019
+ * @returns true if element was removed
1020
+ */
1021
+ remove(element) {
1022
+ const index = this._list.indexOf(element);
1023
+ if (index < 0) {
1024
+ return false;
1025
+ }
1026
+ this._list.splice(index, 1);
1027
+ if (this._list.length > 0) {
1028
+ // if removed item was before head, adjust head
1029
+ if (index < this._pos) {
1030
+ this._pos -= 1;
1031
+ }
1032
+ if (this._circular && this._pos > this._list.length - 1) {
1033
+ this._pos = 0;
1034
+ }
1035
+ }
1036
+ return true;
1037
+ }
1038
+ /**
1039
+ * Reset the head position to the start of the list.
1040
+ */
1041
+ reset() {
1042
+ this._pos = 0;
1043
+ }
1044
+ /**
1045
+ * @returns the current head position in the list, 0 indexed.
1046
+ */
1047
+ pos() {
1048
+ return this._pos;
1049
+ }
1050
+ /**
1051
+ * @returns the current length of the list
1052
+ */
1053
+ length() {
1054
+ return this._list.length;
1055
+ }
1056
+ /**
1057
+ * Clears the list and resets head.
1058
+ */
1059
+ clear() {
1060
+ this._list = [];
1061
+ this._pos = 0;
1062
+ }
1063
+ }
1064
+
1065
+ const DEFAULT_FALLBACK_TIME_MS = 2 * 60 * 1000;
1066
+ const DEFAULT_RECOVERY_TIME_MS = 5 * 60 * 1000;
1067
+ /**
1068
+ * The {@link CompositeDataSource} can combine a number of {@link DataSystemInitializer}s and {@link DataSystemSynchronizer}s
1069
+ * into a single {@link DataSource}, implementing fallback and recovery logic internally to choose where data is sourced from.
1070
+ */
1071
+ class CompositeDataSource {
1072
+ /**
1073
+ * @param initializers factories to create {@link DataSystemInitializer}s, in priority order.
1074
+ * @param synchronizers factories to create {@link DataSystemSynchronizer}s, in priority order.
1075
+ */
1076
+ constructor(initializers, synchronizers, _logger, _transitionConditions = {
1077
+ [DataSourceState.Valid]: {
1078
+ durationMS: DEFAULT_RECOVERY_TIME_MS,
1079
+ transition: 'recover',
1080
+ },
1081
+ [DataSourceState.Interrupted]: {
1082
+ durationMS: DEFAULT_FALLBACK_TIME_MS,
1083
+ transition: 'fallback',
1084
+ },
1085
+ }, _backoff = new DefaultBackoff(1000, 30000)) {
1086
+ this._logger = _logger;
1087
+ this._transitionConditions = _transitionConditions;
1088
+ this._backoff = _backoff;
1089
+ this._stopped = true;
1090
+ this._cancelTokens = [];
1091
+ this._cancellableDelay = (delayMS) => {
1092
+ let timeout;
1093
+ const promise = new Promise((res, _) => {
1094
+ timeout = setTimeout(res, delayMS);
1095
+ });
1096
+ return {
1097
+ promise,
1098
+ cancel() {
1099
+ if (timeout) {
1100
+ clearTimeout(timeout);
1101
+ timeout = undefined;
1102
+ }
1103
+ },
1104
+ };
1105
+ };
1106
+ this._externalTransitionPromise = new Promise((resolveTransition) => {
1107
+ this._externalTransitionResolve = resolveTransition;
1108
+ });
1109
+ this._initPhaseActive = initializers.length > 0; // init phase if we have initializers
1110
+ this._initFactories = new DataSourceList(false, initializers);
1111
+ this._syncFactories = new DataSourceList(true, synchronizers);
1112
+ }
1113
+ async start(dataCallback, statusCallback) {
1114
+ if (!this._stopped) {
1115
+ // don't allow multiple simultaneous runs
1116
+ this._logger?.info('CompositeDataSource already running. Ignoring call to start.');
1117
+ return;
1118
+ }
1119
+ this._stopped = false;
1120
+ this._logger?.debug(`CompositeDataSource starting with (${this._initFactories.length()} initializers, ${this._syncFactories.length()} synchronizers).`);
1121
+ // this wrapper turns status updates from underlying data sources into a valid series of status updates for the consumer of this
1122
+ // composite data source
1123
+ const sanitizedStatusCallback = this._wrapStatusCallbackWithSanitizer(statusCallback);
1124
+ sanitizedStatusCallback(DataSourceState.Initializing);
1125
+ let lastTransition;
1126
+ // eslint-disable-next-line no-constant-condition
1127
+ while (true) {
1128
+ const { dataSource: currentDS, isPrimary, cullDSFactory, } = this._pickDataSource(lastTransition);
1129
+ const internalTransitionPromise = new Promise((transitionResolve) => {
1130
+ if (currentDS) {
1131
+ // these local variables are used for handling automatic transition related to data source status (ex: recovering to primary after
1132
+ // secondary has been valid for N many seconds)
1133
+ let lastState;
1134
+ let cancelScheduledTransition = () => { };
1135
+ // this callback handler can be disabled and ensures only one transition request occurs
1136
+ const callbackHandler = new CallbackHandler((basis, data) => {
1137
+ this._backoff.success();
1138
+ dataCallback(basis, data);
1139
+ if (basis && this._initPhaseActive) {
1140
+ // transition to sync if we get basis during init
1141
+ callbackHandler.disable();
1142
+ this._consumeCancelToken(cancelScheduledTransition);
1143
+ sanitizedStatusCallback(DataSourceState.Interrupted);
1144
+ transitionResolve({ transition: 'switchToSync' });
1145
+ }
1146
+ }, (state, err) => {
1147
+ // When we get a status update, we want to fallback if it is an error. We also want to schedule a transition for some
1148
+ // time in the future if this status remains for some duration (ex: Recover to primary synchronizer after the secondary
1149
+ // synchronizer has been Valid for some time). These scheduled transitions are configurable in the constructor.
1150
+ this._logger?.debug(`CompositeDataSource received state ${state} from underlying data source.`);
1151
+ if (err || state === DataSourceState.Closed) {
1152
+ callbackHandler.disable();
1153
+ if (err.recoverable === false) {
1154
+ // don't use this datasource's factory again
1155
+ cullDSFactory?.();
1156
+ }
1157
+ sanitizedStatusCallback(state, err);
1158
+ this._consumeCancelToken(cancelScheduledTransition);
1159
+ transitionResolve({ transition: 'fallback', err }); // unrecoverable error has occurred, so fallback
1160
+ }
1161
+ else {
1162
+ sanitizedStatusCallback(state);
1163
+ if (state !== lastState) {
1164
+ lastState = state;
1165
+ this._consumeCancelToken(cancelScheduledTransition); // cancel previously scheduled status transition if one was scheduled
1166
+ // primary source cannot recover to itself, so exclude it
1167
+ const condition = this._lookupTransitionCondition(state, isPrimary);
1168
+ if (condition) {
1169
+ const { promise, cancel } = this._cancellableDelay(condition.durationMS);
1170
+ cancelScheduledTransition = cancel;
1171
+ this._cancelTokens.push(cancelScheduledTransition);
1172
+ promise.then(() => {
1173
+ this._consumeCancelToken(cancel);
1174
+ callbackHandler.disable();
1175
+ sanitizedStatusCallback(DataSourceState.Interrupted);
1176
+ transitionResolve({ transition: condition.transition });
1177
+ });
1178
+ }
1179
+ }
1180
+ }
1181
+ });
1182
+ currentDS.start((basis, data) => callbackHandler.dataHandler(basis, data), (status, err) => callbackHandler.statusHandler(status, err));
1183
+ }
1184
+ else {
1185
+ // we don't have a data source to use!
1186
+ transitionResolve({
1187
+ transition: 'stop',
1188
+ err: {
1189
+ name: 'ExhaustedDataSources',
1190
+ message: `CompositeDataSource has exhausted all configured initializers and synchronizers.`,
1191
+ },
1192
+ });
1193
+ }
1194
+ });
1195
+ // await transition triggered by internal data source or an external stop request
1196
+ let transitionRequest = await Promise.race([
1197
+ internalTransitionPromise,
1198
+ this._externalTransitionPromise,
1199
+ ]);
1200
+ // stop the underlying datasource before transitioning to next state
1201
+ currentDS?.stop();
1202
+ if (transitionRequest.err && transitionRequest.transition !== 'stop') {
1203
+ // if the transition was due to an error, throttle the transition
1204
+ const delay = this._backoff.fail();
1205
+ const { promise, cancel: cancelDelay } = this._cancellableDelay(delay);
1206
+ this._cancelTokens.push(cancelDelay);
1207
+ const delayedTransition = promise.then(() => {
1208
+ this._consumeCancelToken(cancelDelay);
1209
+ return transitionRequest;
1210
+ });
1211
+ // race the delayed transition and external transition requests to be responsive
1212
+ transitionRequest = await Promise.race([
1213
+ delayedTransition,
1214
+ this._externalTransitionPromise,
1215
+ ]);
1216
+ // consume the delay cancel token (even if it resolved, need to stop tracking its token)
1217
+ this._consumeCancelToken(cancelDelay);
1218
+ }
1219
+ lastTransition = transitionRequest.transition;
1220
+ if (transitionRequest.transition === 'stop') {
1221
+ // exit the loop, this is intentionally not the sanitized status callback
1222
+ statusCallback(DataSourceState.Closed, transitionRequest.err);
1223
+ break;
1224
+ }
1225
+ }
1226
+ // reset so that run can be called again in the future
1227
+ this._reset();
1228
+ }
1229
+ async stop() {
1230
+ this._cancelTokens.forEach((cancel) => cancel());
1231
+ this._cancelTokens = [];
1232
+ this._externalTransitionResolve?.({ transition: 'stop' });
1233
+ }
1234
+ _reset() {
1235
+ this._stopped = true;
1236
+ this._initPhaseActive = this._initFactories.length() > 0; // init phase if we have initializers;
1237
+ this._initFactories.reset();
1238
+ this._syncFactories.reset();
1239
+ this._externalTransitionPromise = new Promise((tr) => {
1240
+ this._externalTransitionResolve = tr;
1241
+ });
1242
+ // intentionally not resetting the backoff to avoid a code path that could circumvent throttling
1243
+ }
1244
+ /**
1245
+ * Determines the next datasource and returns that datasource as well as a closure to cull the
1246
+ * datasource from the datasource lists. One example where the cull closure is invoked is if the
1247
+ * datasource has an unrecoverable error.
1248
+ */
1249
+ _pickDataSource(transition) {
1250
+ let factory;
1251
+ let isPrimary;
1252
+ switch (transition) {
1253
+ case 'switchToSync':
1254
+ this._initPhaseActive = false; // one way toggle to false, unless this class is reset()
1255
+ this._syncFactories.reset();
1256
+ isPrimary = this._syncFactories.pos() === 0;
1257
+ factory = this._syncFactories.next();
1258
+ break;
1259
+ case 'recover':
1260
+ if (this._initPhaseActive) {
1261
+ this._initFactories.reset();
1262
+ isPrimary = this._initFactories.pos() === 0;
1263
+ factory = this._initFactories.next();
1264
+ }
1265
+ else {
1266
+ this._syncFactories.reset();
1267
+ isPrimary = this._syncFactories.pos() === 0;
1268
+ factory = this._syncFactories.next();
1269
+ }
1270
+ break;
1271
+ case 'fallback':
1272
+ default:
1273
+ if (this._initPhaseActive) {
1274
+ isPrimary = this._initFactories.pos() === 0;
1275
+ factory = this._initFactories.next();
1276
+ }
1277
+ else {
1278
+ isPrimary = this._syncFactories.pos() === 0;
1279
+ factory = this._syncFactories.next();
1280
+ }
1281
+ break;
1282
+ }
1283
+ if (!factory) {
1284
+ return { dataSource: undefined, isPrimary, cullDSFactory: undefined };
1285
+ }
1286
+ return {
1287
+ dataSource: factory(),
1288
+ isPrimary,
1289
+ cullDSFactory: () => {
1290
+ if (factory) {
1291
+ this._syncFactories.remove(factory);
1292
+ }
1293
+ },
1294
+ };
1295
+ }
1296
+ /**
1297
+ * @returns the transition condition for the provided data source state or undefined
1298
+ * if there is no transition condition
1299
+ */
1300
+ _lookupTransitionCondition(state, excludeRecover) {
1301
+ const condition = this._transitionConditions[state];
1302
+ // exclude recovery can happen for certain initializers/synchronizers (ex: the primary synchronizer shouldn't recover to itself)
1303
+ if (excludeRecover && condition?.transition === 'recover') {
1304
+ return undefined;
1305
+ }
1306
+ return condition;
1307
+ }
1308
+ _consumeCancelToken(cancel) {
1309
+ cancel();
1310
+ const index = this._cancelTokens.indexOf(cancel, 0);
1311
+ if (index > -1) {
1312
+ this._cancelTokens.splice(index, 1);
1313
+ }
1314
+ }
1315
+ /**
1316
+ * This wrapper will ensure the following:
1317
+ *
1318
+ * Don't report DataSourceState.Initializing except as first status callback.
1319
+ * Map underlying DataSourceState.Closed to interrupted.
1320
+ * Don't report the same status and error twice in a row.
1321
+ */
1322
+ _wrapStatusCallbackWithSanitizer(statusCallback) {
1323
+ let alreadyReportedInitializing = false;
1324
+ let lastStatus;
1325
+ let lastErr;
1326
+ return (status, err) => {
1327
+ let sanitized = status;
1328
+ // underlying errors, closed state, or off are masked as interrupted while we transition
1329
+ if (status === DataSourceState.Closed) {
1330
+ sanitized = DataSourceState.Interrupted;
1331
+ }
1332
+ // don't report the same combination of values twice in a row
1333
+ if (sanitized === lastStatus && err === lastErr) {
1334
+ return;
1335
+ }
1336
+ if (sanitized === DataSourceState.Initializing) {
1337
+ // don't report initializing again if that has already been reported
1338
+ if (alreadyReportedInitializing) {
1339
+ return;
1340
+ }
1341
+ alreadyReportedInitializing = true;
1342
+ }
1343
+ lastStatus = sanitized;
1344
+ lastErr = err;
1345
+ statusCallback(sanitized, err);
1346
+ };
1347
+ }
1348
+ }
1349
+
814
1350
  var DataSourceErrorKind;
815
1351
  (function (DataSourceErrorKind) {
816
1352
  /// An unexpected error, such as an uncaught exception, further
@@ -879,6 +1415,7 @@ var LDDeliveryStatus;
879
1415
 
880
1416
  var index$1 = /*#__PURE__*/Object.freeze({
881
1417
  __proto__: null,
1418
+ get DataSourceState () { return DataSourceState; },
882
1419
  get LDDeliveryStatus () { return LDDeliveryStatus; },
883
1420
  get LDEventType () { return LDEventType; }
884
1421
  });
@@ -1874,7 +2411,9 @@ function counterKey(event) {
1874
2411
  * @internal
1875
2412
  */
1876
2413
  class EventSummarizer {
1877
- constructor() {
2414
+ constructor(_singleContext = false, _contextFilter) {
2415
+ this._singleContext = _singleContext;
2416
+ this._contextFilter = _contextFilter;
1878
2417
  this._startDate = 0;
1879
2418
  this._endDate = 0;
1880
2419
  this._counters = {};
@@ -1882,6 +2421,9 @@ class EventSummarizer {
1882
2421
  }
1883
2422
  summarizeEvent(event) {
1884
2423
  if (isFeature(event) && !event.excludeFromSummaries) {
2424
+ if (!this._context) {
2425
+ this._context = event.context;
2426
+ }
1885
2427
  const countKey = counterKey(event);
1886
2428
  const counter = this._counters[countKey];
1887
2429
  let kinds = this._contextKinds[event.key];
@@ -1931,14 +2473,19 @@ class EventSummarizer {
1931
2473
  flagSummary.counters.push(counterOut);
1932
2474
  return acc;
1933
2475
  }, {});
1934
- return {
2476
+ const event = {
1935
2477
  startDate: this._startDate,
1936
2478
  endDate: this._endDate,
1937
2479
  features,
1938
2480
  kind: 'summary',
2481
+ context: this._context !== undefined && this._singleContext
2482
+ ? this._contextFilter?.filter(this._context)
2483
+ : undefined,
1939
2484
  };
2485
+ this._clearSummary();
2486
+ return event;
1940
2487
  }
1941
- clearSummary() {
2488
+ _clearSummary() {
1942
2489
  this._startDate = 0;
1943
2490
  this._endDate = 0;
1944
2491
  this._counters = {};
@@ -1953,6 +2500,38 @@ class LDInvalidSDKKeyError extends Error {
1953
2500
  }
1954
2501
  }
1955
2502
 
2503
+ class MultiEventSummarizer {
2504
+ constructor(_contextFilter, _logger) {
2505
+ this._contextFilter = _contextFilter;
2506
+ this._logger = _logger;
2507
+ this._summarizers = {};
2508
+ }
2509
+ summarizeEvent(event) {
2510
+ if (isFeature(event)) {
2511
+ const key = event.context.canonicalUnfilteredJson();
2512
+ if (!key) {
2513
+ if (event.context.valid) {
2514
+ // The context appeared valid, but it could not be hashed.
2515
+ // This is likely because of a cycle in the data.
2516
+ this._logger?.error('Unable to serialize context, likely the context contains a cycle.');
2517
+ }
2518
+ return;
2519
+ }
2520
+ let summarizer = this._summarizers[key];
2521
+ if (!summarizer) {
2522
+ this._summarizers[key] = new EventSummarizer(true, this._contextFilter);
2523
+ summarizer = this._summarizers[key];
2524
+ }
2525
+ summarizer.summarizeEvent(event);
2526
+ }
2527
+ }
2528
+ getSummaries() {
2529
+ const summarizersToFlush = this._summarizers;
2530
+ this._summarizers = {};
2531
+ return Object.values(summarizersToFlush).map((summarizer) => summarizer.getSummary());
2532
+ }
2533
+ }
2534
+
1956
2535
  /**
1957
2536
  * The contents of this file are for event sampling. They are not used for
1958
2537
  * any purpose requiring cryptographic security.
@@ -1973,12 +2552,14 @@ function shouldSample(ratio) {
1973
2552
  return Math.floor(Math.random() * truncated) === 0;
1974
2553
  }
1975
2554
 
2555
+ function isMultiEventSummarizer(summarizer) {
2556
+ return summarizer.getSummaries !== undefined;
2557
+ }
1976
2558
  class EventProcessor {
1977
- constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true) {
2559
+ constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true, summariesPerContext = false) {
1978
2560
  this._config = _config;
1979
2561
  this._contextDeduplicator = _contextDeduplicator;
1980
2562
  this._diagnosticsManager = _diagnosticsManager;
1981
- this._summarizer = new EventSummarizer();
1982
2563
  this._queue = [];
1983
2564
  this._lastKnownPastTime = 0;
1984
2565
  this._droppedEvents = 0;
@@ -1991,6 +2572,12 @@ class EventProcessor {
1991
2572
  this._logger = clientContext.basicConfiguration.logger;
1992
2573
  this._eventSender = new EventSender(clientContext, baseHeaders);
1993
2574
  this._contextFilter = new ContextFilter(_config.allAttributesPrivate, _config.privateAttributes.map((ref) => new AttributeReference(ref)));
2575
+ if (summariesPerContext) {
2576
+ this._summarizer = new MultiEventSummarizer(this._contextFilter, this._logger);
2577
+ }
2578
+ else {
2579
+ this._summarizer = new EventSummarizer();
2580
+ }
1994
2581
  if (start) {
1995
2582
  this.start();
1996
2583
  }
@@ -2042,10 +2629,19 @@ class EventProcessor {
2042
2629
  }
2043
2630
  const eventsToFlush = this._queue;
2044
2631
  this._queue = [];
2045
- const summary = this._summarizer.getSummary();
2046
- this._summarizer.clearSummary();
2047
- if (Object.keys(summary.features).length) {
2048
- eventsToFlush.push(summary);
2632
+ if (isMultiEventSummarizer(this._summarizer)) {
2633
+ const summaries = this._summarizer.getSummaries();
2634
+ summaries.forEach((summary) => {
2635
+ if (Object.keys(summary.features).length) {
2636
+ eventsToFlush.push(summary);
2637
+ }
2638
+ });
2639
+ }
2640
+ else {
2641
+ const summary = this._summarizer.getSummary();
2642
+ if (Object.keys(summary.features).length) {
2643
+ eventsToFlush.push(summary);
2644
+ }
2049
2645
  }
2050
2646
  if (!eventsToFlush.length) {
2051
2647
  return;
@@ -2327,30 +2923,29 @@ class EventFactoryBase {
2327
2923
  }
2328
2924
 
2329
2925
  /**
2330
- * A FDv2 PayloadReader can be used to parse payloads from a stream of FDv2 events. It will send payloads
2926
+ * A FDv2 PayloadProcessor can be used to parse payloads from a stream of FDv2 events. It will send payloads
2331
2927
  * to the PayloadListeners as the payloads are received. Invalid series of events may be dropped silently,
2332
- * but the payload reader will continue to operate.
2928
+ * but the payload processor will continue to operate.
2333
2929
  */
2334
- class PayloadReader {
2930
+ class PayloadProcessor {
2335
2931
  /**
2336
- * Creates a PayloadReader
2932
+ * Creates a PayloadProcessor
2337
2933
  *
2338
- * @param eventStream event stream of FDv2 events
2339
2934
  * @param _objProcessors defines object processors for each object kind.
2340
- * @param _errorHandler that will be called with errors as they are encountered
2935
+ * @param _errorHandler that will be called with parsing errors as they are encountered
2341
2936
  * @param _logger for logging
2342
2937
  */
2343
- constructor(eventStream, _objProcessors, _errorHandler, _logger) {
2938
+ constructor(_objProcessors, _errorHandler, _logger) {
2344
2939
  this._objProcessors = _objProcessors;
2345
2940
  this._errorHandler = _errorHandler;
2346
2941
  this._logger = _logger;
2347
2942
  this._listeners = [];
2348
2943
  this._tempId = undefined;
2349
- this._tempBasis = undefined;
2944
+ this._tempBasis = false;
2350
2945
  this._tempUpdates = [];
2351
2946
  this._processServerIntent = (data) => {
2352
2947
  // clear state in prep for handling data
2353
- this._resetState();
2948
+ this._resetAll();
2354
2949
  // if there's no payloads, return
2355
2950
  if (!data.payloads.length) {
2356
2951
  return;
@@ -2362,8 +2957,11 @@ class PayloadReader {
2362
2957
  this._tempBasis = true;
2363
2958
  break;
2364
2959
  case 'xfer-changes':
2960
+ this._tempBasis = false;
2961
+ break;
2365
2962
  case 'none':
2366
2963
  this._tempBasis = false;
2964
+ this._processIntentNone(payload);
2367
2965
  break;
2368
2966
  default:
2369
2967
  // unrecognized intent code, return
@@ -2373,7 +2971,7 @@ class PayloadReader {
2373
2971
  };
2374
2972
  this._processPutObject = (data) => {
2375
2973
  // if the following properties haven't been provided by now, we should ignore the event
2376
- if (!this._tempId || // server intent hasn't been recieved yet.
2974
+ if (!this._tempId || // server intent hasn't been received yet.
2377
2975
  !data.kind ||
2378
2976
  !data.key ||
2379
2977
  !data.version ||
@@ -2382,7 +2980,7 @@ class PayloadReader {
2382
2980
  }
2383
2981
  const obj = this._processObj(data.kind, data.object);
2384
2982
  if (!obj) {
2385
- this._logger?.warn(`Unable to prcoess object for kind: '${data.kind}'`);
2983
+ this._logger?.warn(`Unable to process object for kind: '${data.kind}'`);
2386
2984
  // ignore unrecognized kinds
2387
2985
  return;
2388
2986
  }
@@ -2407,13 +3005,27 @@ class PayloadReader {
2407
3005
  deleted: true,
2408
3006
  });
2409
3007
  };
3008
+ this._processIntentNone = (intent) => {
3009
+ // if the following properties aren't present ignore the event
3010
+ if (!intent.id || !intent.target) {
3011
+ return;
3012
+ }
3013
+ const payload = {
3014
+ id: intent.id,
3015
+ version: intent.target,
3016
+ basis: false,
3017
+ updates: [], // payload with no updates to hide the intent none concept from the consumer
3018
+ // note: state is absent here as that only appears in payload transferred events
3019
+ };
3020
+ this._listeners.forEach((it) => it(payload));
3021
+ this._resetAfterEmission();
3022
+ };
2410
3023
  this._processPayloadTransferred = (data) => {
2411
3024
  // if the following properties haven't been provided by now, we should reset
2412
- if (!this._tempId || // server intent hasn't been recieved yet.
3025
+ if (!this._tempId || // server intent hasn't been received yet.
2413
3026
  !data.state ||
2414
- !data.version ||
2415
- this._tempBasis === undefined) {
2416
- this._resetState(); // a reset is best defensive action since payload transferred terminates a payload
3027
+ !data.version) {
3028
+ this._resetAll(); // a reset is best defensive action since payload transferred terminates a payload
2417
3029
  return;
2418
3030
  }
2419
3031
  const payload = {
@@ -2424,22 +3036,16 @@ class PayloadReader {
2424
3036
  updates: this._tempUpdates,
2425
3037
  };
2426
3038
  this._listeners.forEach((it) => it(payload));
2427
- this._resetState();
3039
+ this._resetAfterEmission();
2428
3040
  };
2429
3041
  this._processGoodbye = (data) => {
2430
3042
  this._logger?.info(`Goodbye was received from the LaunchDarkly connection with reason: ${data.reason}.`);
2431
- this._resetState();
3043
+ this._resetAll();
2432
3044
  };
2433
3045
  this._processError = (data) => {
2434
- this._logger?.info(`An issue was encountered receiving updates for payload ${this._tempId} with reason: ${data.reason}. Automatic retry will occur.`);
2435
- this._resetState();
3046
+ this._logger?.info(`An issue was encountered receiving updates for payload ${this._tempId} with reason: ${data.reason}.`);
3047
+ this._resetAfterError();
2436
3048
  };
2437
- this._attachHandler(eventStream, 'server-intent', this._processServerIntent);
2438
- this._attachHandler(eventStream, 'put-object', this._processPutObject);
2439
- this._attachHandler(eventStream, 'delete-object', this._processDeleteObject);
2440
- this._attachHandler(eventStream, 'payload-transferred', this._processPayloadTransferred);
2441
- this._attachHandler(eventStream, 'goodbye', this._processGoodbye);
2442
- this._attachHandler(eventStream, 'error', this._processError);
2443
3049
  }
2444
3050
  addPayloadListener(listener) {
2445
3051
  this._listeners.push(listener);
@@ -2450,32 +3056,108 @@ class PayloadReader {
2450
3056
  this._listeners.splice(index, 1);
2451
3057
  }
2452
3058
  }
2453
- _attachHandler(stream, eventName, processor) {
3059
+ /**
3060
+ * Gives the {@link PayloadProcessor} a series of events that it will statefully, incrementally process.
3061
+ * This may lead to listeners being invoked as necessary.
3062
+ * @param events to be processed (can be a single element)
3063
+ */
3064
+ processEvents(events) {
3065
+ events.forEach((event) => {
3066
+ switch (event.event) {
3067
+ case 'server-intent': {
3068
+ this._processServerIntent(event.data);
3069
+ break;
3070
+ }
3071
+ case 'put-object': {
3072
+ this._processPutObject(event.data);
3073
+ break;
3074
+ }
3075
+ case 'delete-object': {
3076
+ this._processDeleteObject(event.data);
3077
+ break;
3078
+ }
3079
+ case 'payload-transferred': {
3080
+ this._processPayloadTransferred(event.data);
3081
+ break;
3082
+ }
3083
+ case 'goodbye': {
3084
+ this._processGoodbye(event.data);
3085
+ break;
3086
+ }
3087
+ case 'error': {
3088
+ this._processError(event.data);
3089
+ break;
3090
+ }
3091
+ }
3092
+ });
3093
+ }
3094
+ _processObj(kind, jsonObj) {
3095
+ return this._objProcessors[kind]?.(jsonObj);
3096
+ }
3097
+ _resetAfterEmission() {
3098
+ this._tempBasis = false;
3099
+ this._tempUpdates = [];
3100
+ }
3101
+ _resetAfterError() {
3102
+ this._tempUpdates = [];
3103
+ }
3104
+ _resetAll() {
3105
+ this._tempId = undefined;
3106
+ this._tempBasis = false;
3107
+ this._tempUpdates = [];
3108
+ }
3109
+ }
3110
+
3111
+ /**
3112
+ * A FDv2 PayloadStreamReader can be used to parse payloads from a stream of FDv2 events. See {@link PayloadProcessor}
3113
+ * for more details.
3114
+ */
3115
+ class PayloadStreamReader {
3116
+ /**
3117
+ * Creates a PayloadStreamReader
3118
+ *
3119
+ * @param eventStream event stream of FDv2 events
3120
+ * @param _objProcessors defines object processors for each object kind.
3121
+ * @param _errorHandler that will be called with parsing errors as they are encountered
3122
+ * @param _logger for logging
3123
+ */
3124
+ constructor(eventStream, _objProcessors, _errorHandler, _logger) {
3125
+ this._errorHandler = _errorHandler;
3126
+ this._logger = _logger;
3127
+ this._attachHandler(eventStream, 'server-intent');
3128
+ this._attachHandler(eventStream, 'put-object');
3129
+ this._attachHandler(eventStream, 'delete-object');
3130
+ this._attachHandler(eventStream, 'payload-transferred');
3131
+ this._attachHandler(eventStream, 'goodbye');
3132
+ this._attachHandler(eventStream, 'error');
3133
+ this._payloadProcessor = new PayloadProcessor(_objProcessors, _errorHandler, _logger);
3134
+ }
3135
+ addPayloadListener(listener) {
3136
+ this._payloadProcessor.addPayloadListener(listener);
3137
+ }
3138
+ removePayloadListener(listener) {
3139
+ this._payloadProcessor.removePayloadListener(listener);
3140
+ }
3141
+ _attachHandler(stream, eventName) {
2454
3142
  stream.addEventListener(eventName, async (event) => {
2455
3143
  if (event?.data) {
2456
3144
  this._logger?.debug(`Received ${eventName} event. Data is ${event.data}`);
2457
3145
  try {
2458
- processor(JSON.parse(event.data));
3146
+ this._payloadProcessor.processEvents([
3147
+ { event: eventName, data: JSON.parse(event.data) },
3148
+ ]);
2459
3149
  }
2460
3150
  catch {
2461
3151
  this._logger?.error(`Stream received data that was unable to be processed in "${eventName}" message`);
2462
3152
  this._logger?.debug(`Data follows: ${event.data}`);
2463
- this._errorHandler?.(DataSourceErrorKind.InvalidData, 'Malformed data in event stream');
3153
+ this._errorHandler?.(DataSourceErrorKind.InvalidData, 'Malformed data in EventStream.');
2464
3154
  }
2465
3155
  }
2466
3156
  else {
2467
- this._errorHandler?.(DataSourceErrorKind.Unknown, 'Unexpected message from event stream');
3157
+ this._errorHandler?.(DataSourceErrorKind.Unknown, 'Event from EventStream missing data.');
2468
3158
  }
2469
3159
  });
2470
3160
  }
2471
- _processObj(kind, jsonObj) {
2472
- return this._objProcessors[kind]?.(jsonObj);
2473
- }
2474
- _resetState() {
2475
- this._tempId = undefined;
2476
- this._tempBasis = undefined;
2477
- this._tempUpdates = [];
2478
- }
2479
3161
  }
2480
3162
 
2481
3163
  /**
@@ -2496,6 +3178,47 @@ function initMetadataFromHeaders(initHeaders) {
2496
3178
  return undefined;
2497
3179
  }
2498
3180
 
3181
+ const UNKNOWN_PLUGIN_NAME = 'unknown plugin';
3182
+ function safeGetName(logger, plugin) {
3183
+ try {
3184
+ return plugin.getMetadata().name || UNKNOWN_PLUGIN_NAME;
3185
+ }
3186
+ catch {
3187
+ logger.error(`Exception thrown getting metadata for plugin. Unable to get plugin name.`);
3188
+ return UNKNOWN_PLUGIN_NAME;
3189
+ }
3190
+ }
3191
+
3192
+ function safeGetHooks(logger, environmentMetadata, plugins) {
3193
+ const hooks = [];
3194
+ plugins.forEach((plugin) => {
3195
+ try {
3196
+ const pluginHooks = plugin.getHooks?.(environmentMetadata);
3197
+ if (pluginHooks === undefined) {
3198
+ logger.error(`Plugin ${safeGetName(logger, plugin)} returned undefined from getHooks.`);
3199
+ }
3200
+ else if (pluginHooks && pluginHooks.length > 0) {
3201
+ hooks.push(...pluginHooks);
3202
+ }
3203
+ }
3204
+ catch (error) {
3205
+ logger.error(`Exception thrown getting hooks for plugin ${safeGetName(logger, plugin)}. Unable to get hooks.`);
3206
+ }
3207
+ });
3208
+ return hooks;
3209
+ }
3210
+
3211
+ function safeRegisterPlugins(logger, environmentMetadata, client, plugins) {
3212
+ plugins.forEach((plugin) => {
3213
+ try {
3214
+ plugin.register(client, environmentMetadata);
3215
+ }
3216
+ catch (error) {
3217
+ logger.error(`Exception thrown registering plugin ${safeGetName(logger, plugin)}.`);
3218
+ }
3219
+ });
3220
+ }
3221
+
2499
3222
  var index = /*#__PURE__*/Object.freeze({
2500
3223
  __proto__: null,
2501
3224
  ClientMessages: ClientMessages,
@@ -2507,13 +3230,18 @@ var index = /*#__PURE__*/Object.freeze({
2507
3230
  InputEvalEvent: InputEvalEvent,
2508
3231
  InputIdentifyEvent: InputIdentifyEvent,
2509
3232
  NullEventProcessor: NullEventProcessor,
2510
- PayloadReader: PayloadReader,
3233
+ PayloadProcessor: PayloadProcessor,
3234
+ PayloadStreamReader: PayloadStreamReader,
3235
+ canonicalize: canonicalize,
2511
3236
  initMetadataFromHeaders: initMetadataFromHeaders,
2512
3237
  isLegacyUser: isLegacyUser,
2513
3238
  isMultiKind: isMultiKind,
2514
3239
  isSingleKind: isSingleKind,
3240
+ safeGetHooks: safeGetHooks,
3241
+ safeGetName: safeGetName,
3242
+ safeRegisterPlugins: safeRegisterPlugins,
2515
3243
  shouldSample: shouldSample
2516
3244
  });
2517
3245
 
2518
- export { ApplicationTags, AttributeReference, AutoEnvAttributes, BasicLogger, ClientContext, Context, ContextFilter, DataSourceErrorKind, DateValidator, FactoryOrInstance, Function, KindValidator, LDClientError, LDFileDataSourceError, LDPollingError, LDStreamingError, LDTimeoutError, LDUnexpectedResponseError, NullableBoolean, NumberWithMinimum, OptionMessages, SafeLogger, ServiceEndpoints, StringMatchingRegex, Type, TypeArray, TypeValidators, base64UrlEncode, cancelableTimedPromise, clone, createSafeLogger, debounce, deepCompact, defaultHeaders, fastDeepEqual, getEventsUri, getPollingUri, getStreamingUri, httpErrorMessage, index as internal, isHttpLocallyRecoverable, isHttpRecoverable, noop, secondsToMillis, shouldRetry, sleep, index$1 as subsystem, timedPromise };
3246
+ export { ApplicationTags, AttributeReference, AutoEnvAttributes, BasicLogger, ClientContext, CompositeDataSource, Context, ContextFilter, DataSourceErrorKind, DateValidator, DefaultBackoff, FactoryOrInstance, Function, KindValidator, LDClientError, LDFileDataSourceError, LDPollingError, LDStreamingError, LDTimeoutError, LDUnexpectedResponseError, NullableBoolean, NumberWithMinimum, OptionMessages, SafeLogger, ServiceEndpoints, StringMatchingRegex, Type, TypeArray, TypeValidators, base64UrlEncode, cancelableTimedPromise, clone, createSafeLogger, debounce, deepCompact, defaultHeaders, fastDeepEqual, getEventsUri, getPollingUri, getStreamingUri, httpErrorMessage, index as internal, isHttpLocallyRecoverable, isHttpRecoverable, noop, secondsToMillis, shouldRetry, sleep, index$1 as subsystem, timedPromise };
2519
3247
  //# sourceMappingURL=index.mjs.map