@launchdarkly/js-sdk-common 2.16.0 → 2.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 (88) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/api/index.d.ts +1 -0
  3. package/dist/cjs/api/index.d.ts.map +1 -1
  4. package/dist/cjs/api/integrations/index.d.ts +2 -0
  5. package/dist/cjs/api/integrations/index.d.ts.map +1 -0
  6. package/dist/cjs/api/integrations/plugins.d.ts +128 -0
  7. package/dist/cjs/api/integrations/plugins.d.ts.map +1 -0
  8. package/dist/cjs/api/subsystem/DataSystem/CallbackHandler.d.ts +16 -0
  9. package/dist/cjs/api/subsystem/DataSystem/CallbackHandler.d.ts.map +1 -0
  10. package/dist/cjs/api/subsystem/DataSystem/DataSource.d.ts +21 -0
  11. package/dist/cjs/api/subsystem/DataSystem/DataSource.d.ts.map +1 -0
  12. package/dist/cjs/api/subsystem/DataSystem/index.d.ts +2 -0
  13. package/dist/cjs/api/subsystem/DataSystem/index.d.ts.map +1 -0
  14. package/dist/cjs/api/subsystem/index.d.ts +2 -1
  15. package/dist/cjs/api/subsystem/index.d.ts.map +1 -1
  16. package/dist/cjs/datasource/Backoff.d.ts +46 -0
  17. package/dist/cjs/datasource/Backoff.d.ts.map +1 -0
  18. package/dist/cjs/datasource/CompositeDataSource.d.ts +62 -0
  19. package/dist/cjs/datasource/CompositeDataSource.d.ts.map +1 -0
  20. package/dist/cjs/datasource/dataSourceList.d.ts +47 -0
  21. package/dist/cjs/datasource/dataSourceList.d.ts.map +1 -0
  22. package/dist/cjs/datasource/index.d.ts +3 -1
  23. package/dist/cjs/datasource/index.d.ts.map +1 -1
  24. package/dist/cjs/index.cjs +648 -38
  25. package/dist/cjs/index.cjs.map +1 -1
  26. package/dist/cjs/index.d.ts +2 -2
  27. package/dist/cjs/index.d.ts.map +1 -1
  28. package/dist/cjs/internal/fdv2/index.d.ts +3 -2
  29. package/dist/cjs/internal/fdv2/index.d.ts.map +1 -1
  30. package/dist/cjs/internal/fdv2/{payloadReader.d.ts → payloadProcessor.d.ts} +27 -16
  31. package/dist/cjs/internal/fdv2/payloadProcessor.d.ts.map +1 -0
  32. package/dist/cjs/internal/fdv2/payloadStreamReader.d.ts +31 -0
  33. package/dist/cjs/internal/fdv2/payloadStreamReader.d.ts.map +1 -0
  34. package/dist/cjs/internal/index.d.ts +1 -0
  35. package/dist/cjs/internal/index.d.ts.map +1 -1
  36. package/dist/cjs/internal/plugins/index.d.ts +4 -0
  37. package/dist/cjs/internal/plugins/index.d.ts.map +1 -0
  38. package/dist/cjs/internal/plugins/safeGetHooks.d.ts +4 -0
  39. package/dist/cjs/internal/plugins/safeGetHooks.d.ts.map +1 -0
  40. package/dist/cjs/internal/plugins/safeGetName.d.ts +4 -0
  41. package/dist/cjs/internal/plugins/safeGetName.d.ts.map +1 -0
  42. package/dist/cjs/internal/plugins/safeRegisterPlugins.d.ts +4 -0
  43. package/dist/cjs/internal/plugins/safeRegisterPlugins.d.ts.map +1 -0
  44. package/dist/esm/api/index.d.ts +1 -0
  45. package/dist/esm/api/index.d.ts.map +1 -1
  46. package/dist/esm/api/integrations/index.d.ts +2 -0
  47. package/dist/esm/api/integrations/index.d.ts.map +1 -0
  48. package/dist/esm/api/integrations/plugins.d.ts +128 -0
  49. package/dist/esm/api/integrations/plugins.d.ts.map +1 -0
  50. package/dist/esm/api/subsystem/DataSystem/CallbackHandler.d.ts +16 -0
  51. package/dist/esm/api/subsystem/DataSystem/CallbackHandler.d.ts.map +1 -0
  52. package/dist/esm/api/subsystem/DataSystem/DataSource.d.ts +21 -0
  53. package/dist/esm/api/subsystem/DataSystem/DataSource.d.ts.map +1 -0
  54. package/dist/esm/api/subsystem/DataSystem/index.d.ts +2 -0
  55. package/dist/esm/api/subsystem/DataSystem/index.d.ts.map +1 -0
  56. package/dist/esm/api/subsystem/index.d.ts +2 -1
  57. package/dist/esm/api/subsystem/index.d.ts.map +1 -1
  58. package/dist/esm/datasource/Backoff.d.ts +46 -0
  59. package/dist/esm/datasource/Backoff.d.ts.map +1 -0
  60. package/dist/esm/datasource/CompositeDataSource.d.ts +62 -0
  61. package/dist/esm/datasource/CompositeDataSource.d.ts.map +1 -0
  62. package/dist/esm/datasource/dataSourceList.d.ts +47 -0
  63. package/dist/esm/datasource/dataSourceList.d.ts.map +1 -0
  64. package/dist/esm/datasource/index.d.ts +3 -1
  65. package/dist/esm/datasource/index.d.ts.map +1 -1
  66. package/dist/esm/index.d.ts +2 -2
  67. package/dist/esm/index.d.ts.map +1 -1
  68. package/dist/esm/index.mjs +647 -39
  69. package/dist/esm/index.mjs.map +1 -1
  70. package/dist/esm/internal/fdv2/index.d.ts +3 -2
  71. package/dist/esm/internal/fdv2/index.d.ts.map +1 -1
  72. package/dist/esm/internal/fdv2/{payloadReader.d.ts → payloadProcessor.d.ts} +27 -16
  73. package/dist/esm/internal/fdv2/payloadProcessor.d.ts.map +1 -0
  74. package/dist/esm/internal/fdv2/payloadStreamReader.d.ts +31 -0
  75. package/dist/esm/internal/fdv2/payloadStreamReader.d.ts.map +1 -0
  76. package/dist/esm/internal/index.d.ts +1 -0
  77. package/dist/esm/internal/index.d.ts.map +1 -1
  78. package/dist/esm/internal/plugins/index.d.ts +4 -0
  79. package/dist/esm/internal/plugins/index.d.ts.map +1 -0
  80. package/dist/esm/internal/plugins/safeGetHooks.d.ts +4 -0
  81. package/dist/esm/internal/plugins/safeGetHooks.d.ts.map +1 -0
  82. package/dist/esm/internal/plugins/safeGetName.d.ts +4 -0
  83. package/dist/esm/internal/plugins/safeGetName.d.ts.map +1 -0
  84. package/dist/esm/internal/plugins/safeRegisterPlugins.d.ts +4 -0
  85. package/dist/esm/internal/plugins/safeRegisterPlugins.d.ts.map +1 -0
  86. package/package.json +1 -1
  87. package/dist/cjs/internal/fdv2/payloadReader.d.ts.map +0 -1
  88. package/dist/esm/internal/fdv2/payloadReader.d.ts.map +0 -1
@@ -813,6 +813,482 @@ class ContextFilter {
813
813
  }
814
814
  }
815
815
 
816
+ const MAX_RETRY_DELAY = 30 * 1000; // Maximum retry delay 30 seconds.
817
+ const JITTER_RATIO = 0.5; // Delay should be 50%-100% of calculated time.
818
+ /**
819
+ * Implements exponential backoff and jitter. This class tracks successful connections and failures
820
+ * and produces a retry delay.
821
+ *
822
+ * It does not start any timers or directly control a connection.
823
+ *
824
+ * The backoff follows an exponential backoff scheme with 50% jitter starting at
825
+ * initialRetryDelayMillis and capping at MAX_RETRY_DELAY. If RESET_INTERVAL has elapsed after a
826
+ * success, without an intervening faulure, then the backoff is reset to initialRetryDelayMillis.
827
+ */
828
+ class DefaultBackoff {
829
+ constructor(initialRetryDelayMillis, _retryResetIntervalMillis, _random = Math.random) {
830
+ this._retryResetIntervalMillis = _retryResetIntervalMillis;
831
+ this._random = _random;
832
+ this._retryCount = 0;
833
+ // Initial retry delay cannot be 0.
834
+ this._initialRetryDelayMillis = Math.max(1, initialRetryDelayMillis);
835
+ this._maxExponent = Math.ceil(Math.log2(MAX_RETRY_DELAY / this._initialRetryDelayMillis));
836
+ }
837
+ _backoff() {
838
+ const exponent = Math.min(this._retryCount, this._maxExponent);
839
+ const delay = this._initialRetryDelayMillis * 2 ** exponent;
840
+ return Math.min(delay, MAX_RETRY_DELAY);
841
+ }
842
+ _jitter(computedDelayMillis) {
843
+ return computedDelayMillis - Math.trunc(this._random() * JITTER_RATIO * computedDelayMillis);
844
+ }
845
+ /**
846
+ * This function should be called when a connection attempt is successful.
847
+ *
848
+ * @param timeStampMs The time of the success. Used primarily for testing, when not provided
849
+ * the current time is used.
850
+ */
851
+ success(timeStampMs = Date.now()) {
852
+ this._activeSince = timeStampMs;
853
+ }
854
+ /**
855
+ * This function should be called when a connection fails. It returns the a delay, in
856
+ * milliseconds, after which a reconnection attempt should be made.
857
+ *
858
+ * @param timeStampMs The time of the success. Used primarily for testing, when not provided
859
+ * the current time is used.
860
+ * @returns The delay before the next connection attempt.
861
+ */
862
+ fail(timeStampMs = Date.now()) {
863
+ // If the last successful connection was active for more than the RESET_INTERVAL, then we
864
+ // return to the initial retry delay.
865
+ if (this._activeSince !== undefined &&
866
+ timeStampMs - this._activeSince > this._retryResetIntervalMillis) {
867
+ this._retryCount = 0;
868
+ }
869
+ this._activeSince = undefined;
870
+ const delay = this._jitter(this._backoff());
871
+ this._retryCount += 1;
872
+ return delay;
873
+ }
874
+ }
875
+
876
+ /**
877
+ * Handler that connects the current {@link DataSource} to the {@link CompositeDataSource}. A single
878
+ * {@link CallbackHandler} should only be given to one {@link DataSource}. Use {@link disable()} to
879
+ * prevent additional callback events.
880
+ */
881
+ class CallbackHandler {
882
+ constructor(_dataCallback, _statusCallback) {
883
+ this._dataCallback = _dataCallback;
884
+ this._statusCallback = _statusCallback;
885
+ this._disabled = false;
886
+ }
887
+ disable() {
888
+ this._disabled = true;
889
+ }
890
+ async dataHandler(basis, data) {
891
+ if (this._disabled) {
892
+ return;
893
+ }
894
+ // TODO: SDK-1044 track selector for future synchronizer to use
895
+ // report data up
896
+ this._dataCallback(basis, data);
897
+ }
898
+ async statusHandler(status, err) {
899
+ if (this._disabled) {
900
+ return;
901
+ }
902
+ this._statusCallback(status, err);
903
+ }
904
+ }
905
+
906
+ // TODO: refactor client-sdk to use this enum
907
+ var DataSourceState;
908
+ (function (DataSourceState) {
909
+ // Positive confirmation of connection/data receipt
910
+ DataSourceState[DataSourceState["Valid"] = 0] = "Valid";
911
+ // Spinning up to make first connection attempt
912
+ DataSourceState[DataSourceState["Initializing"] = 1] = "Initializing";
913
+ // Transient issue, automatic retry is expected
914
+ DataSourceState[DataSourceState["Interrupted"] = 2] = "Interrupted";
915
+ // Data source was closed and will not retry automatically.
916
+ DataSourceState[DataSourceState["Closed"] = 3] = "Closed";
917
+ })(DataSourceState || (DataSourceState = {}));
918
+
919
+ /**
920
+ * Helper class for {@link CompositeDataSource} to manage iterating on data sources and removing them on the fly.
921
+ */
922
+ class DataSourceList {
923
+ /**
924
+ * @param circular whether to loop off the end of the list back to the start
925
+ * @param initialList of content
926
+ */
927
+ constructor(circular, initialList) {
928
+ this._list = initialList ? [...initialList] : [];
929
+ this._circular = circular;
930
+ this._pos = 0;
931
+ }
932
+ /**
933
+ * Returns the current head and then iterates.
934
+ */
935
+ next() {
936
+ if (this._list.length <= 0 || this._pos >= this._list.length) {
937
+ return undefined;
938
+ }
939
+ const result = this._list[this._pos];
940
+ if (this._circular) {
941
+ this._pos = (this._pos + 1) % this._list.length;
942
+ }
943
+ else {
944
+ this._pos += 1;
945
+ }
946
+ return result;
947
+ }
948
+ /**
949
+ * Replaces all elements with the provided list and resets the position of head to the start.
950
+ *
951
+ * @param input that will replace existing list
952
+ */
953
+ replace(input) {
954
+ this._list = [...input];
955
+ this._pos = 0;
956
+ }
957
+ /**
958
+ * 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.
959
+ *
960
+ * @param element to remove
961
+ * @returns true if element was removed
962
+ */
963
+ remove(element) {
964
+ const index = this._list.indexOf(element);
965
+ if (index < 0) {
966
+ return false;
967
+ }
968
+ this._list.splice(index, 1);
969
+ if (this._list.length > 0) {
970
+ // if removed item was before head, adjust head
971
+ if (index < this._pos) {
972
+ this._pos -= 1;
973
+ }
974
+ if (this._circular && this._pos > this._list.length - 1) {
975
+ this._pos = 0;
976
+ }
977
+ }
978
+ return true;
979
+ }
980
+ /**
981
+ * Reset the head position to the start of the list.
982
+ */
983
+ reset() {
984
+ this._pos = 0;
985
+ }
986
+ /**
987
+ * @returns the current head position in the list, 0 indexed.
988
+ */
989
+ pos() {
990
+ return this._pos;
991
+ }
992
+ /**
993
+ * @returns the current length of the list
994
+ */
995
+ length() {
996
+ return this._list.length;
997
+ }
998
+ /**
999
+ * Clears the list and resets head.
1000
+ */
1001
+ clear() {
1002
+ this._list = [];
1003
+ this._pos = 0;
1004
+ }
1005
+ }
1006
+
1007
+ const DEFAULT_FALLBACK_TIME_MS = 2 * 60 * 1000;
1008
+ const DEFAULT_RECOVERY_TIME_MS = 5 * 60 * 1000;
1009
+ /**
1010
+ * The {@link CompositeDataSource} can combine a number of {@link DataSystemInitializer}s and {@link DataSystemSynchronizer}s
1011
+ * into a single {@link DataSource}, implementing fallback and recovery logic internally to choose where data is sourced from.
1012
+ */
1013
+ class CompositeDataSource {
1014
+ /**
1015
+ * @param initializers factories to create {@link DataSystemInitializer}s, in priority order.
1016
+ * @param synchronizers factories to create {@link DataSystemSynchronizer}s, in priority order.
1017
+ */
1018
+ constructor(initializers, synchronizers, _logger, _transitionConditions = {
1019
+ [DataSourceState.Valid]: {
1020
+ durationMS: DEFAULT_RECOVERY_TIME_MS,
1021
+ transition: 'recover',
1022
+ },
1023
+ [DataSourceState.Interrupted]: {
1024
+ durationMS: DEFAULT_FALLBACK_TIME_MS,
1025
+ transition: 'fallback',
1026
+ },
1027
+ }, _backoff = new DefaultBackoff(1000, 30000)) {
1028
+ this._logger = _logger;
1029
+ this._transitionConditions = _transitionConditions;
1030
+ this._backoff = _backoff;
1031
+ this._stopped = true;
1032
+ this._cancelTokens = [];
1033
+ this._cancellableDelay = (delayMS) => {
1034
+ let timeout;
1035
+ const promise = new Promise((res, _) => {
1036
+ timeout = setTimeout(res, delayMS);
1037
+ });
1038
+ return {
1039
+ promise,
1040
+ cancel() {
1041
+ if (timeout) {
1042
+ clearTimeout(timeout);
1043
+ timeout = undefined;
1044
+ }
1045
+ },
1046
+ };
1047
+ };
1048
+ this._externalTransitionPromise = new Promise((resolveTransition) => {
1049
+ this._externalTransitionResolve = resolveTransition;
1050
+ });
1051
+ this._initPhaseActive = initializers.length > 0; // init phase if we have initializers
1052
+ this._initFactories = new DataSourceList(false, initializers);
1053
+ this._syncFactories = new DataSourceList(true, synchronizers);
1054
+ }
1055
+ async start(dataCallback, statusCallback) {
1056
+ if (!this._stopped) {
1057
+ // don't allow multiple simultaneous runs
1058
+ this._logger?.info('CompositeDataSource already running. Ignoring call to start.');
1059
+ return;
1060
+ }
1061
+ this._stopped = false;
1062
+ this._logger?.debug(`CompositeDataSource starting with (${this._initFactories.length()} initializers, ${this._syncFactories.length()} synchronizers).`);
1063
+ // this wrapper turns status updates from underlying data sources into a valid series of status updates for the consumer of this
1064
+ // composite data source
1065
+ const sanitizedStatusCallback = this._wrapStatusCallbackWithSanitizer(statusCallback);
1066
+ sanitizedStatusCallback(DataSourceState.Initializing);
1067
+ let lastTransition;
1068
+ // eslint-disable-next-line no-constant-condition
1069
+ while (true) {
1070
+ const { dataSource: currentDS, isPrimary, cullDSFactory, } = this._pickDataSource(lastTransition);
1071
+ const internalTransitionPromise = new Promise((transitionResolve) => {
1072
+ if (currentDS) {
1073
+ // these local variables are used for handling automatic transition related to data source status (ex: recovering to primary after
1074
+ // secondary has been valid for N many seconds)
1075
+ let lastState;
1076
+ let cancelScheduledTransition = () => { };
1077
+ // this callback handler can be disabled and ensures only one transition request occurs
1078
+ const callbackHandler = new CallbackHandler((basis, data) => {
1079
+ this._backoff.success();
1080
+ dataCallback(basis, data);
1081
+ if (basis && this._initPhaseActive) {
1082
+ // transition to sync if we get basis during init
1083
+ callbackHandler.disable();
1084
+ this._consumeCancelToken(cancelScheduledTransition);
1085
+ sanitizedStatusCallback(DataSourceState.Interrupted);
1086
+ transitionResolve({ transition: 'switchToSync' });
1087
+ }
1088
+ }, (state, err) => {
1089
+ // When we get a status update, we want to fallback if it is an error. We also want to schedule a transition for some
1090
+ // time in the future if this status remains for some duration (ex: Recover to primary synchronizer after the secondary
1091
+ // synchronizer has been Valid for some time). These scheduled transitions are configurable in the constructor.
1092
+ this._logger?.debug(`CompositeDataSource received state ${state} from underlying data source.`);
1093
+ if (err || state === DataSourceState.Closed) {
1094
+ callbackHandler.disable();
1095
+ if (err.recoverable === false) {
1096
+ // don't use this datasource's factory again
1097
+ cullDSFactory?.();
1098
+ }
1099
+ sanitizedStatusCallback(state, err);
1100
+ this._consumeCancelToken(cancelScheduledTransition);
1101
+ transitionResolve({ transition: 'fallback', err }); // unrecoverable error has occurred, so fallback
1102
+ }
1103
+ else {
1104
+ sanitizedStatusCallback(state);
1105
+ if (state !== lastState) {
1106
+ lastState = state;
1107
+ this._consumeCancelToken(cancelScheduledTransition); // cancel previously scheduled status transition if one was scheduled
1108
+ // primary source cannot recover to itself, so exclude it
1109
+ const condition = this._lookupTransitionCondition(state, isPrimary);
1110
+ if (condition) {
1111
+ const { promise, cancel } = this._cancellableDelay(condition.durationMS);
1112
+ cancelScheduledTransition = cancel;
1113
+ this._cancelTokens.push(cancelScheduledTransition);
1114
+ promise.then(() => {
1115
+ this._consumeCancelToken(cancel);
1116
+ callbackHandler.disable();
1117
+ sanitizedStatusCallback(DataSourceState.Interrupted);
1118
+ transitionResolve({ transition: condition.transition });
1119
+ });
1120
+ }
1121
+ }
1122
+ }
1123
+ });
1124
+ currentDS.start((basis, data) => callbackHandler.dataHandler(basis, data), (status, err) => callbackHandler.statusHandler(status, err));
1125
+ }
1126
+ else {
1127
+ // we don't have a data source to use!
1128
+ transitionResolve({
1129
+ transition: 'stop',
1130
+ err: {
1131
+ name: 'ExhaustedDataSources',
1132
+ message: `CompositeDataSource has exhausted all configured initializers and synchronizers.`,
1133
+ },
1134
+ });
1135
+ }
1136
+ });
1137
+ // await transition triggered by internal data source or an external stop request
1138
+ let transitionRequest = await Promise.race([
1139
+ internalTransitionPromise,
1140
+ this._externalTransitionPromise,
1141
+ ]);
1142
+ // stop the underlying datasource before transitioning to next state
1143
+ currentDS?.stop();
1144
+ if (transitionRequest.err && transitionRequest.transition !== 'stop') {
1145
+ // if the transition was due to an error, throttle the transition
1146
+ const delay = this._backoff.fail();
1147
+ const { promise, cancel: cancelDelay } = this._cancellableDelay(delay);
1148
+ this._cancelTokens.push(cancelDelay);
1149
+ const delayedTransition = promise.then(() => {
1150
+ this._consumeCancelToken(cancelDelay);
1151
+ return transitionRequest;
1152
+ });
1153
+ // race the delayed transition and external transition requests to be responsive
1154
+ transitionRequest = await Promise.race([
1155
+ delayedTransition,
1156
+ this._externalTransitionPromise,
1157
+ ]);
1158
+ // consume the delay cancel token (even if it resolved, need to stop tracking its token)
1159
+ this._consumeCancelToken(cancelDelay);
1160
+ }
1161
+ lastTransition = transitionRequest.transition;
1162
+ if (transitionRequest.transition === 'stop') {
1163
+ // exit the loop, this is intentionally not the sanitized status callback
1164
+ statusCallback(DataSourceState.Closed, transitionRequest.err);
1165
+ break;
1166
+ }
1167
+ }
1168
+ // reset so that run can be called again in the future
1169
+ this._reset();
1170
+ }
1171
+ async stop() {
1172
+ this._cancelTokens.forEach((cancel) => cancel());
1173
+ this._cancelTokens = [];
1174
+ this._externalTransitionResolve?.({ transition: 'stop' });
1175
+ }
1176
+ _reset() {
1177
+ this._stopped = true;
1178
+ this._initPhaseActive = this._initFactories.length() > 0; // init phase if we have initializers;
1179
+ this._initFactories.reset();
1180
+ this._syncFactories.reset();
1181
+ this._externalTransitionPromise = new Promise((tr) => {
1182
+ this._externalTransitionResolve = tr;
1183
+ });
1184
+ // intentionally not resetting the backoff to avoid a code path that could circumvent throttling
1185
+ }
1186
+ /**
1187
+ * Determines the next datasource and returns that datasource as well as a closure to cull the
1188
+ * datasource from the datasource lists. One example where the cull closure is invoked is if the
1189
+ * datasource has an unrecoverable error.
1190
+ */
1191
+ _pickDataSource(transition) {
1192
+ let factory;
1193
+ let isPrimary;
1194
+ switch (transition) {
1195
+ case 'switchToSync':
1196
+ this._initPhaseActive = false; // one way toggle to false, unless this class is reset()
1197
+ this._syncFactories.reset();
1198
+ isPrimary = this._syncFactories.pos() === 0;
1199
+ factory = this._syncFactories.next();
1200
+ break;
1201
+ case 'recover':
1202
+ if (this._initPhaseActive) {
1203
+ this._initFactories.reset();
1204
+ isPrimary = this._initFactories.pos() === 0;
1205
+ factory = this._initFactories.next();
1206
+ }
1207
+ else {
1208
+ this._syncFactories.reset();
1209
+ isPrimary = this._syncFactories.pos() === 0;
1210
+ factory = this._syncFactories.next();
1211
+ }
1212
+ break;
1213
+ case 'fallback':
1214
+ default:
1215
+ if (this._initPhaseActive) {
1216
+ isPrimary = this._initFactories.pos() === 0;
1217
+ factory = this._initFactories.next();
1218
+ }
1219
+ else {
1220
+ isPrimary = this._syncFactories.pos() === 0;
1221
+ factory = this._syncFactories.next();
1222
+ }
1223
+ break;
1224
+ }
1225
+ if (!factory) {
1226
+ return { dataSource: undefined, isPrimary, cullDSFactory: undefined };
1227
+ }
1228
+ return {
1229
+ dataSource: factory(),
1230
+ isPrimary,
1231
+ cullDSFactory: () => {
1232
+ if (factory) {
1233
+ this._syncFactories.remove(factory);
1234
+ }
1235
+ },
1236
+ };
1237
+ }
1238
+ /**
1239
+ * @returns the transition condition for the provided data source state or undefined
1240
+ * if there is no transition condition
1241
+ */
1242
+ _lookupTransitionCondition(state, excludeRecover) {
1243
+ const condition = this._transitionConditions[state];
1244
+ // exclude recovery can happen for certain initializers/synchronizers (ex: the primary synchronizer shouldn't recover to itself)
1245
+ if (excludeRecover && condition?.transition === 'recover') {
1246
+ return undefined;
1247
+ }
1248
+ return condition;
1249
+ }
1250
+ _consumeCancelToken(cancel) {
1251
+ cancel();
1252
+ const index = this._cancelTokens.indexOf(cancel, 0);
1253
+ if (index > -1) {
1254
+ this._cancelTokens.splice(index, 1);
1255
+ }
1256
+ }
1257
+ /**
1258
+ * This wrapper will ensure the following:
1259
+ *
1260
+ * Don't report DataSourceState.Initializing except as first status callback.
1261
+ * Map underlying DataSourceState.Closed to interrupted.
1262
+ * Don't report the same status and error twice in a row.
1263
+ */
1264
+ _wrapStatusCallbackWithSanitizer(statusCallback) {
1265
+ let alreadyReportedInitializing = false;
1266
+ let lastStatus;
1267
+ let lastErr;
1268
+ return (status, err) => {
1269
+ let sanitized = status;
1270
+ // underlying errors, closed state, or off are masked as interrupted while we transition
1271
+ if (status === DataSourceState.Closed) {
1272
+ sanitized = DataSourceState.Interrupted;
1273
+ }
1274
+ // don't report the same combination of values twice in a row
1275
+ if (sanitized === lastStatus && err === lastErr) {
1276
+ return;
1277
+ }
1278
+ if (sanitized === DataSourceState.Initializing) {
1279
+ // don't report initializing again if that has already been reported
1280
+ if (alreadyReportedInitializing) {
1281
+ return;
1282
+ }
1283
+ alreadyReportedInitializing = true;
1284
+ }
1285
+ lastStatus = sanitized;
1286
+ lastErr = err;
1287
+ statusCallback(sanitized, err);
1288
+ };
1289
+ }
1290
+ }
1291
+
816
1292
  exports.DataSourceErrorKind = void 0;
817
1293
  (function (DataSourceErrorKind) {
818
1294
  /// An unexpected error, such as an uncaught exception, further
@@ -881,6 +1357,7 @@ var LDDeliveryStatus;
881
1357
 
882
1358
  var index$1 = /*#__PURE__*/Object.freeze({
883
1359
  __proto__: null,
1360
+ get DataSourceState () { return DataSourceState; },
884
1361
  get LDDeliveryStatus () { return LDDeliveryStatus; },
885
1362
  get LDEventType () { return LDEventType; }
886
1363
  });
@@ -2329,30 +2806,29 @@ class EventFactoryBase {
2329
2806
  }
2330
2807
 
2331
2808
  /**
2332
- * A FDv2 PayloadReader can be used to parse payloads from a stream of FDv2 events. It will send payloads
2809
+ * A FDv2 PayloadProcessor can be used to parse payloads from a stream of FDv2 events. It will send payloads
2333
2810
  * to the PayloadListeners as the payloads are received. Invalid series of events may be dropped silently,
2334
- * but the payload reader will continue to operate.
2811
+ * but the payload processor will continue to operate.
2335
2812
  */
2336
- class PayloadReader {
2813
+ class PayloadProcessor {
2337
2814
  /**
2338
- * Creates a PayloadReader
2815
+ * Creates a PayloadProcessor
2339
2816
  *
2340
- * @param eventStream event stream of FDv2 events
2341
2817
  * @param _objProcessors defines object processors for each object kind.
2342
- * @param _errorHandler that will be called with errors as they are encountered
2818
+ * @param _errorHandler that will be called with parsing errors as they are encountered
2343
2819
  * @param _logger for logging
2344
2820
  */
2345
- constructor(eventStream, _objProcessors, _errorHandler, _logger) {
2821
+ constructor(_objProcessors, _errorHandler, _logger) {
2346
2822
  this._objProcessors = _objProcessors;
2347
2823
  this._errorHandler = _errorHandler;
2348
2824
  this._logger = _logger;
2349
2825
  this._listeners = [];
2350
2826
  this._tempId = undefined;
2351
- this._tempBasis = undefined;
2827
+ this._tempBasis = false;
2352
2828
  this._tempUpdates = [];
2353
2829
  this._processServerIntent = (data) => {
2354
2830
  // clear state in prep for handling data
2355
- this._resetState();
2831
+ this._resetAll();
2356
2832
  // if there's no payloads, return
2357
2833
  if (!data.payloads.length) {
2358
2834
  return;
@@ -2364,8 +2840,11 @@ class PayloadReader {
2364
2840
  this._tempBasis = true;
2365
2841
  break;
2366
2842
  case 'xfer-changes':
2843
+ this._tempBasis = false;
2844
+ break;
2367
2845
  case 'none':
2368
2846
  this._tempBasis = false;
2847
+ this._processIntentNone(payload);
2369
2848
  break;
2370
2849
  default:
2371
2850
  // unrecognized intent code, return
@@ -2375,7 +2854,7 @@ class PayloadReader {
2375
2854
  };
2376
2855
  this._processPutObject = (data) => {
2377
2856
  // if the following properties haven't been provided by now, we should ignore the event
2378
- if (!this._tempId || // server intent hasn't been recieved yet.
2857
+ if (!this._tempId || // server intent hasn't been received yet.
2379
2858
  !data.kind ||
2380
2859
  !data.key ||
2381
2860
  !data.version ||
@@ -2384,7 +2863,7 @@ class PayloadReader {
2384
2863
  }
2385
2864
  const obj = this._processObj(data.kind, data.object);
2386
2865
  if (!obj) {
2387
- this._logger?.warn(`Unable to prcoess object for kind: '${data.kind}'`);
2866
+ this._logger?.warn(`Unable to process object for kind: '${data.kind}'`);
2388
2867
  // ignore unrecognized kinds
2389
2868
  return;
2390
2869
  }
@@ -2409,13 +2888,27 @@ class PayloadReader {
2409
2888
  deleted: true,
2410
2889
  });
2411
2890
  };
2891
+ this._processIntentNone = (intent) => {
2892
+ // if the following properties aren't present ignore the event
2893
+ if (!intent.id || !intent.target) {
2894
+ return;
2895
+ }
2896
+ const payload = {
2897
+ id: intent.id,
2898
+ version: intent.target,
2899
+ basis: false,
2900
+ updates: [], // payload with no updates to hide the intent none concept from the consumer
2901
+ // note: state is absent here as that only appears in payload transferred events
2902
+ };
2903
+ this._listeners.forEach((it) => it(payload));
2904
+ this._resetAfterEmission();
2905
+ };
2412
2906
  this._processPayloadTransferred = (data) => {
2413
2907
  // if the following properties haven't been provided by now, we should reset
2414
- if (!this._tempId || // server intent hasn't been recieved yet.
2908
+ if (!this._tempId || // server intent hasn't been received yet.
2415
2909
  !data.state ||
2416
- !data.version ||
2417
- this._tempBasis === undefined) {
2418
- this._resetState(); // a reset is best defensive action since payload transferred terminates a payload
2910
+ !data.version) {
2911
+ this._resetAll(); // a reset is best defensive action since payload transferred terminates a payload
2419
2912
  return;
2420
2913
  }
2421
2914
  const payload = {
@@ -2426,22 +2919,16 @@ class PayloadReader {
2426
2919
  updates: this._tempUpdates,
2427
2920
  };
2428
2921
  this._listeners.forEach((it) => it(payload));
2429
- this._resetState();
2922
+ this._resetAfterEmission();
2430
2923
  };
2431
2924
  this._processGoodbye = (data) => {
2432
2925
  this._logger?.info(`Goodbye was received from the LaunchDarkly connection with reason: ${data.reason}.`);
2433
- this._resetState();
2926
+ this._resetAll();
2434
2927
  };
2435
2928
  this._processError = (data) => {
2436
- this._logger?.info(`An issue was encountered receiving updates for payload ${this._tempId} with reason: ${data.reason}. Automatic retry will occur.`);
2437
- this._resetState();
2929
+ this._logger?.info(`An issue was encountered receiving updates for payload ${this._tempId} with reason: ${data.reason}.`);
2930
+ this._resetAfterError();
2438
2931
  };
2439
- this._attachHandler(eventStream, 'server-intent', this._processServerIntent);
2440
- this._attachHandler(eventStream, 'put-object', this._processPutObject);
2441
- this._attachHandler(eventStream, 'delete-object', this._processDeleteObject);
2442
- this._attachHandler(eventStream, 'payload-transferred', this._processPayloadTransferred);
2443
- this._attachHandler(eventStream, 'goodbye', this._processGoodbye);
2444
- this._attachHandler(eventStream, 'error', this._processError);
2445
2932
  }
2446
2933
  addPayloadListener(listener) {
2447
2934
  this._listeners.push(listener);
@@ -2452,32 +2939,108 @@ class PayloadReader {
2452
2939
  this._listeners.splice(index, 1);
2453
2940
  }
2454
2941
  }
2455
- _attachHandler(stream, eventName, processor) {
2942
+ /**
2943
+ * Gives the {@link PayloadProcessor} a series of events that it will statefully, incrementally process.
2944
+ * This may lead to listeners being invoked as necessary.
2945
+ * @param events to be processed (can be a single element)
2946
+ */
2947
+ processEvents(events) {
2948
+ events.forEach((event) => {
2949
+ switch (event.event) {
2950
+ case 'server-intent': {
2951
+ this._processServerIntent(event.data);
2952
+ break;
2953
+ }
2954
+ case 'put-object': {
2955
+ this._processPutObject(event.data);
2956
+ break;
2957
+ }
2958
+ case 'delete-object': {
2959
+ this._processDeleteObject(event.data);
2960
+ break;
2961
+ }
2962
+ case 'payload-transferred': {
2963
+ this._processPayloadTransferred(event.data);
2964
+ break;
2965
+ }
2966
+ case 'goodbye': {
2967
+ this._processGoodbye(event.data);
2968
+ break;
2969
+ }
2970
+ case 'error': {
2971
+ this._processError(event.data);
2972
+ break;
2973
+ }
2974
+ }
2975
+ });
2976
+ }
2977
+ _processObj(kind, jsonObj) {
2978
+ return this._objProcessors[kind]?.(jsonObj);
2979
+ }
2980
+ _resetAfterEmission() {
2981
+ this._tempBasis = false;
2982
+ this._tempUpdates = [];
2983
+ }
2984
+ _resetAfterError() {
2985
+ this._tempUpdates = [];
2986
+ }
2987
+ _resetAll() {
2988
+ this._tempId = undefined;
2989
+ this._tempBasis = false;
2990
+ this._tempUpdates = [];
2991
+ }
2992
+ }
2993
+
2994
+ /**
2995
+ * A FDv2 PayloadStreamReader can be used to parse payloads from a stream of FDv2 events. See {@link PayloadProcessor}
2996
+ * for more details.
2997
+ */
2998
+ class PayloadStreamReader {
2999
+ /**
3000
+ * Creates a PayloadStreamReader
3001
+ *
3002
+ * @param eventStream event stream of FDv2 events
3003
+ * @param _objProcessors defines object processors for each object kind.
3004
+ * @param _errorHandler that will be called with parsing errors as they are encountered
3005
+ * @param _logger for logging
3006
+ */
3007
+ constructor(eventStream, _objProcessors, _errorHandler, _logger) {
3008
+ this._errorHandler = _errorHandler;
3009
+ this._logger = _logger;
3010
+ this._attachHandler(eventStream, 'server-intent');
3011
+ this._attachHandler(eventStream, 'put-object');
3012
+ this._attachHandler(eventStream, 'delete-object');
3013
+ this._attachHandler(eventStream, 'payload-transferred');
3014
+ this._attachHandler(eventStream, 'goodbye');
3015
+ this._attachHandler(eventStream, 'error');
3016
+ this._payloadProcessor = new PayloadProcessor(_objProcessors, _errorHandler, _logger);
3017
+ }
3018
+ addPayloadListener(listener) {
3019
+ this._payloadProcessor.addPayloadListener(listener);
3020
+ }
3021
+ removePayloadListener(listener) {
3022
+ this._payloadProcessor.removePayloadListener(listener);
3023
+ }
3024
+ _attachHandler(stream, eventName) {
2456
3025
  stream.addEventListener(eventName, async (event) => {
2457
3026
  if (event?.data) {
2458
3027
  this._logger?.debug(`Received ${eventName} event. Data is ${event.data}`);
2459
3028
  try {
2460
- processor(JSON.parse(event.data));
3029
+ this._payloadProcessor.processEvents([
3030
+ { event: eventName, data: JSON.parse(event.data) },
3031
+ ]);
2461
3032
  }
2462
3033
  catch {
2463
3034
  this._logger?.error(`Stream received data that was unable to be processed in "${eventName}" message`);
2464
3035
  this._logger?.debug(`Data follows: ${event.data}`);
2465
- this._errorHandler?.(exports.DataSourceErrorKind.InvalidData, 'Malformed data in event stream');
3036
+ this._errorHandler?.(exports.DataSourceErrorKind.InvalidData, 'Malformed data in EventStream.');
2466
3037
  }
2467
3038
  }
2468
3039
  else {
2469
- this._errorHandler?.(exports.DataSourceErrorKind.Unknown, 'Unexpected message from event stream');
3040
+ this._errorHandler?.(exports.DataSourceErrorKind.Unknown, 'Event from EventStream missing data.');
2470
3041
  }
2471
3042
  });
2472
3043
  }
2473
- _processObj(kind, jsonObj) {
2474
- return this._objProcessors[kind]?.(jsonObj);
2475
- }
2476
- _resetState() {
2477
- this._tempId = undefined;
2478
- this._tempBasis = undefined;
2479
- this._tempUpdates = [];
2480
- }
2481
3044
  }
2482
3045
 
2483
3046
  /**
@@ -2498,6 +3061,47 @@ function initMetadataFromHeaders(initHeaders) {
2498
3061
  return undefined;
2499
3062
  }
2500
3063
 
3064
+ const UNKNOWN_PLUGIN_NAME = 'unknown plugin';
3065
+ function safeGetName(logger, plugin) {
3066
+ try {
3067
+ return plugin.getMetadata().name || UNKNOWN_PLUGIN_NAME;
3068
+ }
3069
+ catch {
3070
+ logger.error(`Exception thrown getting metadata for plugin. Unable to get plugin name.`);
3071
+ return UNKNOWN_PLUGIN_NAME;
3072
+ }
3073
+ }
3074
+
3075
+ function safeGetHooks(logger, environmentMetadata, plugins) {
3076
+ const hooks = [];
3077
+ plugins.forEach((plugin) => {
3078
+ try {
3079
+ const pluginHooks = plugin.getHooks?.(environmentMetadata);
3080
+ if (pluginHooks === undefined) {
3081
+ logger.error(`Plugin ${safeGetName(logger, plugin)} returned undefined from getHooks.`);
3082
+ }
3083
+ else if (pluginHooks && pluginHooks.length > 0) {
3084
+ hooks.push(...pluginHooks);
3085
+ }
3086
+ }
3087
+ catch (error) {
3088
+ logger.error(`Exception thrown getting hooks for plugin ${safeGetName(logger, plugin)}. Unable to get hooks.`);
3089
+ }
3090
+ });
3091
+ return hooks;
3092
+ }
3093
+
3094
+ function safeRegisterPlugins(logger, environmentMetadata, client, plugins) {
3095
+ plugins.forEach((plugin) => {
3096
+ try {
3097
+ plugin.register(client, environmentMetadata);
3098
+ }
3099
+ catch (error) {
3100
+ logger.error(`Exception thrown registering plugin ${safeGetName(logger, plugin)}.`);
3101
+ }
3102
+ });
3103
+ }
3104
+
2501
3105
  var index = /*#__PURE__*/Object.freeze({
2502
3106
  __proto__: null,
2503
3107
  ClientMessages: ClientMessages,
@@ -2509,11 +3113,15 @@ var index = /*#__PURE__*/Object.freeze({
2509
3113
  InputEvalEvent: InputEvalEvent,
2510
3114
  InputIdentifyEvent: InputIdentifyEvent,
2511
3115
  NullEventProcessor: NullEventProcessor,
2512
- PayloadReader: PayloadReader,
3116
+ PayloadProcessor: PayloadProcessor,
3117
+ PayloadStreamReader: PayloadStreamReader,
2513
3118
  initMetadataFromHeaders: initMetadataFromHeaders,
2514
3119
  isLegacyUser: isLegacyUser,
2515
3120
  isMultiKind: isMultiKind,
2516
3121
  isSingleKind: isSingleKind,
3122
+ safeGetHooks: safeGetHooks,
3123
+ safeGetName: safeGetName,
3124
+ safeRegisterPlugins: safeRegisterPlugins,
2517
3125
  shouldSample: shouldSample
2518
3126
  });
2519
3127
 
@@ -2521,9 +3129,11 @@ exports.ApplicationTags = ApplicationTags;
2521
3129
  exports.AttributeReference = AttributeReference;
2522
3130
  exports.BasicLogger = BasicLogger;
2523
3131
  exports.ClientContext = ClientContext;
3132
+ exports.CompositeDataSource = CompositeDataSource;
2524
3133
  exports.Context = Context;
2525
3134
  exports.ContextFilter = ContextFilter;
2526
3135
  exports.DateValidator = DateValidator;
3136
+ exports.DefaultBackoff = DefaultBackoff;
2527
3137
  exports.FactoryOrInstance = FactoryOrInstance;
2528
3138
  exports.Function = Function;
2529
3139
  exports.KindValidator = KindValidator;