@splitsoftware/splitio-commons 1.17.0 → 1.17.1-rc.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 (165) hide show
  1. package/CHANGES.txt +6 -0
  2. package/cjs/evaluator/matchers/index.js +3 -1
  3. package/cjs/evaluator/matchers/large_segment.js +16 -0
  4. package/cjs/evaluator/matchers/matcherTypes.js +1 -0
  5. package/cjs/evaluator/matchersTransform/index.js +4 -1
  6. package/cjs/evaluator/matchersTransform/segment.js +3 -1
  7. package/cjs/logger/constants.js +2 -2
  8. package/cjs/logger/messages/info.js +1 -1
  9. package/cjs/logger/messages/warn.js +1 -1
  10. package/cjs/readiness/readinessManager.js +5 -6
  11. package/cjs/readiness/sdkReadinessManager.js +5 -6
  12. package/cjs/sdkClient/sdkClientMethodCS.js +2 -2
  13. package/cjs/sdkClient/sdkClientMethodCSWithTT.js +2 -2
  14. package/cjs/sdkFactory/index.js +1 -1
  15. package/cjs/services/splitApi.js +5 -5
  16. package/cjs/storages/AbstractSegmentsCacheSync.js +41 -12
  17. package/cjs/storages/AbstractSplitsCacheSync.js +2 -1
  18. package/cjs/storages/KeyBuilderCS.js +23 -5
  19. package/cjs/storages/dataLoader.js +1 -1
  20. package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +29 -52
  21. package/cjs/storages/inLocalStorage/index.js +6 -2
  22. package/cjs/storages/inMemory/InMemoryStorageCS.js +5 -0
  23. package/cjs/storages/inMemory/MySegmentsCacheInMemory.js +9 -40
  24. package/cjs/storages/inMemory/SplitsCacheInMemory.js +8 -8
  25. package/cjs/storages/inMemory/TelemetryCacheInMemory.js +7 -10
  26. package/cjs/storages/pluggable/inMemoryWrapper.js +1 -1
  27. package/cjs/sync/polling/fetchers/mySegmentsFetcher.js +5 -8
  28. package/cjs/sync/polling/fetchers/segmentChangesFetcher.js +1 -1
  29. package/cjs/sync/polling/pollingManagerCS.js +1 -1
  30. package/cjs/sync/polling/syncTasks/mySegmentsSyncTask.js +2 -2
  31. package/cjs/sync/polling/updaters/mySegmentsUpdater.js +15 -21
  32. package/cjs/sync/streaming/AuthClient/index.js +1 -1
  33. package/cjs/sync/streaming/SSEHandler/index.js +3 -5
  34. package/cjs/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.js +107 -48
  35. package/cjs/sync/streaming/constants.js +3 -3
  36. package/cjs/sync/streaming/parseUtils.js +14 -9
  37. package/cjs/sync/streaming/pushManager.js +69 -67
  38. package/cjs/utils/constants/index.js +5 -4
  39. package/cjs/utils/settingsValidation/index.js +2 -1
  40. package/esm/evaluator/matchers/index.js +3 -1
  41. package/esm/evaluator/matchers/large_segment.js +12 -0
  42. package/esm/evaluator/matchers/matcherTypes.js +1 -0
  43. package/esm/evaluator/matchersTransform/index.js +4 -1
  44. package/esm/evaluator/matchersTransform/segment.js +3 -1
  45. package/esm/logger/constants.js +1 -1
  46. package/esm/logger/messages/info.js +1 -1
  47. package/esm/logger/messages/warn.js +1 -1
  48. package/esm/readiness/readinessManager.js +5 -6
  49. package/esm/readiness/sdkReadinessManager.js +5 -6
  50. package/esm/sdkClient/sdkClientMethodCS.js +2 -2
  51. package/esm/sdkClient/sdkClientMethodCSWithTT.js +2 -2
  52. package/esm/sdkFactory/index.js +1 -1
  53. package/esm/services/splitApi.js +6 -6
  54. package/esm/storages/AbstractSegmentsCacheSync.js +41 -12
  55. package/esm/storages/AbstractSplitsCacheSync.js +3 -2
  56. package/esm/storages/KeyBuilderCS.js +21 -4
  57. package/esm/storages/dataLoader.js +1 -1
  58. package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +29 -52
  59. package/esm/storages/inLocalStorage/index.js +7 -3
  60. package/esm/storages/inMemory/InMemoryStorageCS.js +5 -0
  61. package/esm/storages/inMemory/MySegmentsCacheInMemory.js +9 -40
  62. package/esm/storages/inMemory/SplitsCacheInMemory.js +8 -8
  63. package/esm/storages/inMemory/TelemetryCacheInMemory.js +7 -10
  64. package/esm/storages/pluggable/inMemoryWrapper.js +1 -1
  65. package/esm/sync/polling/fetchers/mySegmentsFetcher.js +5 -8
  66. package/esm/sync/polling/fetchers/segmentChangesFetcher.js +1 -1
  67. package/esm/sync/polling/pollingManagerCS.js +1 -1
  68. package/esm/sync/polling/syncTasks/mySegmentsSyncTask.js +2 -2
  69. package/esm/sync/polling/updaters/mySegmentsUpdater.js +15 -21
  70. package/esm/sync/streaming/AuthClient/index.js +1 -1
  71. package/esm/sync/streaming/SSEHandler/index.js +4 -6
  72. package/esm/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.js +108 -49
  73. package/esm/sync/streaming/constants.js +2 -2
  74. package/esm/sync/streaming/parseUtils.js +12 -8
  75. package/esm/sync/streaming/pushManager.js +72 -70
  76. package/esm/utils/constants/index.js +3 -2
  77. package/esm/utils/settingsValidation/index.js +2 -1
  78. package/package.json +1 -1
  79. package/src/dtos/types.ts +21 -7
  80. package/src/evaluator/matchers/index.ts +2 -0
  81. package/src/evaluator/matchers/large_segment.ts +18 -0
  82. package/src/evaluator/matchers/matcherTypes.ts +1 -0
  83. package/src/evaluator/matchersTransform/index.ts +4 -1
  84. package/src/evaluator/matchersTransform/segment.ts +5 -3
  85. package/src/logger/constants.ts +1 -1
  86. package/src/logger/messages/info.ts +1 -1
  87. package/src/logger/messages/warn.ts +1 -1
  88. package/src/readiness/readinessManager.ts +7 -5
  89. package/src/readiness/sdkReadinessManager.ts +7 -7
  90. package/src/readiness/types.ts +2 -2
  91. package/src/sdkClient/sdkClientMethodCS.ts +2 -2
  92. package/src/sdkClient/sdkClientMethodCSWithTT.ts +2 -2
  93. package/src/sdkFactory/index.ts +1 -1
  94. package/src/services/splitApi.ts +7 -7
  95. package/src/services/splitHttpClient.ts +1 -1
  96. package/src/services/types.ts +2 -2
  97. package/src/storages/AbstractSegmentsCacheSync.ts +53 -12
  98. package/src/storages/AbstractSplitsCacheSync.ts +4 -3
  99. package/src/storages/KeyBuilderCS.ts +34 -5
  100. package/src/storages/dataLoader.ts +1 -1
  101. package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +29 -59
  102. package/src/storages/inLocalStorage/index.ts +8 -4
  103. package/src/storages/inMemory/InMemoryStorageCS.ts +5 -0
  104. package/src/storages/inMemory/MySegmentsCacheInMemory.ts +10 -44
  105. package/src/storages/inMemory/SplitsCacheInMemory.ts +7 -8
  106. package/src/storages/inMemory/TelemetryCacheInMemory.ts +7 -11
  107. package/src/storages/pluggable/inMemoryWrapper.ts +1 -1
  108. package/src/storages/types.ts +11 -7
  109. package/src/sync/polling/fetchers/mySegmentsFetcher.ts +8 -10
  110. package/src/sync/polling/fetchers/segmentChangesFetcher.ts +1 -1
  111. package/src/sync/polling/fetchers/types.ts +3 -2
  112. package/src/sync/polling/pollingManagerCS.ts +4 -4
  113. package/src/sync/polling/syncTasks/mySegmentsSyncTask.ts +4 -5
  114. package/src/sync/polling/types.ts +7 -6
  115. package/src/sync/polling/updaters/mySegmentsUpdater.ts +19 -22
  116. package/src/sync/streaming/AuthClient/index.ts +1 -1
  117. package/src/sync/streaming/SSEClient/index.ts +4 -6
  118. package/src/sync/streaming/SSEHandler/index.ts +5 -8
  119. package/src/sync/streaming/SSEHandler/types.ts +15 -15
  120. package/src/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.ts +116 -49
  121. package/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts +1 -1
  122. package/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +1 -1
  123. package/src/sync/streaming/UpdateWorkers/types.ts +2 -2
  124. package/src/sync/streaming/constants.ts +2 -2
  125. package/src/sync/streaming/parseUtils.ts +19 -11
  126. package/src/sync/streaming/pushManager.ts +73 -72
  127. package/src/sync/streaming/types.ts +10 -10
  128. package/src/sync/submitters/types.ts +8 -5
  129. package/src/utils/constants/index.ts +3 -2
  130. package/src/utils/settingsValidation/index.ts +3 -2
  131. package/src/utils/settingsValidation/types.ts +1 -1
  132. package/types/dtos/types.d.ts +18 -7
  133. package/types/evaluator/matchersTransform/segment.d.ts +2 -2
  134. package/types/logger/constants.d.ts +1 -1
  135. package/types/readiness/readinessManager.d.ts +2 -2
  136. package/types/readiness/sdkReadinessManager.d.ts +2 -3
  137. package/types/readiness/types.d.ts +2 -2
  138. package/types/services/splitApi.d.ts +1 -1
  139. package/types/services/splitHttpClient.d.ts +1 -1
  140. package/types/services/types.d.ts +2 -2
  141. package/types/storages/AbstractSegmentsCacheSync.d.ts +9 -11
  142. package/types/storages/AbstractSplitsCacheSync.d.ts +1 -1
  143. package/types/storages/KeyBuilderCS.d.ts +9 -2
  144. package/types/storages/inLocalStorage/MySegmentsCacheInLocal.d.ts +4 -14
  145. package/types/storages/inMemory/MySegmentsCacheInMemory.d.ts +3 -9
  146. package/types/storages/inMemory/SplitsCacheInMemory.d.ts +1 -1
  147. package/types/storages/inMemory/TelemetryCacheInMemory.d.ts +4 -6
  148. package/types/storages/pluggable/inMemoryWrapper.d.ts +1 -1
  149. package/types/storages/types.d.ts +7 -5
  150. package/types/sync/polling/fetchers/mySegmentsFetcher.d.ts +2 -2
  151. package/types/sync/polling/fetchers/types.d.ts +2 -2
  152. package/types/sync/polling/syncTasks/mySegmentsSyncTask.d.ts +2 -2
  153. package/types/sync/polling/types.d.ts +7 -4
  154. package/types/sync/polling/updaters/mySegmentsUpdater.d.ts +4 -3
  155. package/types/sync/streaming/SSEHandler/types.d.ts +16 -14
  156. package/types/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.d.ts +4 -2
  157. package/types/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.d.ts +2 -1
  158. package/types/sync/streaming/UpdateWorkers/SplitsUpdateWorker.d.ts +3 -2
  159. package/types/sync/streaming/UpdateWorkers/types.d.ts +2 -2
  160. package/types/sync/streaming/constants.d.ts +2 -2
  161. package/types/sync/streaming/parseUtils.d.ts +4 -5
  162. package/types/sync/streaming/types.d.ts +8 -8
  163. package/types/sync/submitters/types.d.ts +7 -4
  164. package/types/utils/constants/index.d.ts +3 -2
  165. package/types/utils/settingsValidation/types.d.ts +1 -1
@@ -1,66 +1,133 @@
1
1
  import { IMySegmentsSyncTask, MySegmentsData } from '../../polling/types';
2
2
  import { Backoff } from '../../../utils/Backoff';
3
3
  import { IUpdateWorker } from './types';
4
- import { MY_SEGMENT } from '../../../utils/constants';
5
4
  import { ITelemetryTracker } from '../../../trackers/types';
5
+ import { MEMBERSHIPS } from '../../../utils/constants';
6
+ import { ISegmentsCacheSync, IStorageSync } from '../../../storages/types';
7
+ import { ILogger } from '../../../logger/types';
8
+ import { FETCH_BACKOFF_MAX_RETRIES } from './constants';
9
+ import { MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../constants';
6
10
 
7
11
  /**
8
12
  * MySegmentsUpdateWorker factory
9
13
  */
10
- export function MySegmentsUpdateWorker(mySegmentsSyncTask: IMySegmentsSyncTask, telemetryTracker: ITelemetryTracker): IUpdateWorker {
11
-
12
- let maxChangeNumber = 0; // keeps the maximum changeNumber among queued events
13
- let currentChangeNumber = -1;
14
- let handleNewEvent = false;
15
- let isHandlingEvent: boolean;
16
- let _segmentsData: MySegmentsData | undefined; // keeps the segmentsData (if included in notification payload) from the queued event with maximum changeNumber
17
- const backoff = new Backoff(__handleMySegmentsUpdateCall);
18
-
19
- function __handleMySegmentsUpdateCall() {
20
- isHandlingEvent = true;
21
- if (maxChangeNumber > currentChangeNumber) {
22
- handleNewEvent = false;
23
- const currentMaxChangeNumber = maxChangeNumber;
24
-
25
- // fetch mySegments revalidating data if cached
26
- mySegmentsSyncTask.execute(_segmentsData, true).then((result) => {
27
- if (!isHandlingEvent) return; // halt if `stop` has been called
28
- if (result !== false) {// Unlike `Splits|SegmentsUpdateWorker`, we cannot use `mySegmentsCache.getChangeNumber` since `/mySegments` endpoint doesn't provide this value.
29
- if (_segmentsData) telemetryTracker.trackUpdatesFromSSE(MY_SEGMENT);
30
- currentChangeNumber = Math.max(currentChangeNumber, currentMaxChangeNumber); // use `currentMaxChangeNumber`, in case that `maxChangeNumber` was updated during fetch.
31
- }
32
- if (handleNewEvent) {
33
- __handleMySegmentsUpdateCall();
34
- } else {
35
- backoff.scheduleCall();
36
- }
37
- });
38
- } else {
39
- isHandlingEvent = false;
14
+ export function MySegmentsUpdateWorker(log: ILogger, storage: Pick<IStorageSync, 'segments' | 'largeSegments'>, mySegmentsSyncTask: IMySegmentsSyncTask, telemetryTracker: ITelemetryTracker): IUpdateWorker<[mySegmentsData?: Pick<MySegmentsData, 'type' | 'cn'>, payload?: Pick<MySegmentsData, 'added' | 'removed'>, delay?: number]> {
15
+
16
+ let _delay: undefined | number;
17
+ let _delayTimeoutID: any;
18
+
19
+ function createUpdateWorker(mySegmentsCache: ISegmentsCacheSync) {
20
+
21
+ let maxChangeNumber = 0; // keeps the maximum changeNumber among queued events
22
+ let currentChangeNumber = -1;
23
+ let handleNewEvent = false;
24
+ let isHandlingEvent: boolean;
25
+ let cdnBypass: boolean;
26
+ let _segmentsData: MySegmentsData | undefined; // keeps the segmentsData (if included in notification payload) from the queued event with maximum changeNumber
27
+ const backoff = new Backoff(__handleMySegmentsUpdateCall);
28
+
29
+ function __handleMySegmentsUpdateCall() {
30
+ isHandlingEvent = true;
31
+ if (maxChangeNumber > Math.max(currentChangeNumber, mySegmentsCache.getChangeNumber())) {
32
+ handleNewEvent = false;
33
+ const currentMaxChangeNumber = maxChangeNumber;
34
+
35
+ // fetch mySegments revalidating data if cached
36
+ const syncTask = _delay ?
37
+ new Promise(res => {
38
+ _delayTimeoutID = setTimeout(() => {
39
+ _delay = undefined;
40
+ mySegmentsSyncTask.execute(_segmentsData, true, cdnBypass ? maxChangeNumber : undefined).then(res);
41
+ }, _delay);
42
+ }) :
43
+ mySegmentsSyncTask.execute(_segmentsData, true, cdnBypass ? maxChangeNumber : undefined);
44
+
45
+ syncTask.then((result) => {
46
+ if (!isHandlingEvent) return; // halt if `stop` has been called
47
+ if (result !== false) { // Unlike `Splits|SegmentsUpdateWorker`, `mySegmentsCache.getChangeNumber` can be -1, since `/memberships` change number is optional
48
+ const storageChangeNumber = mySegmentsCache.getChangeNumber();
49
+ currentChangeNumber = storageChangeNumber > -1 ?
50
+ storageChangeNumber :
51
+ Math.max(currentChangeNumber, currentMaxChangeNumber); // use `currentMaxChangeNumber`, in case that `maxChangeNumber` was updated during fetch.
52
+ }
53
+ if (handleNewEvent) {
54
+ __handleMySegmentsUpdateCall();
55
+ } else {
56
+ if (_segmentsData) telemetryTracker.trackUpdatesFromSSE(MEMBERSHIPS);
57
+
58
+ const attempts = backoff.attempts + 1;
59
+
60
+ if (maxChangeNumber <= currentChangeNumber) {
61
+ log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`);
62
+ isHandlingEvent = false;
63
+ return;
64
+ }
65
+
66
+ if (attempts < FETCH_BACKOFF_MAX_RETRIES) {
67
+ backoff.scheduleCall();
68
+ return;
69
+ }
70
+
71
+ if (cdnBypass) {
72
+ log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`);
73
+ isHandlingEvent = false;
74
+ } else {
75
+ backoff.reset();
76
+ cdnBypass = true;
77
+ __handleMySegmentsUpdateCall();
78
+ }
79
+ }
80
+ });
81
+ } else {
82
+ isHandlingEvent = false;
83
+ }
40
84
  }
85
+
86
+ return {
87
+ /**
88
+ * Invoked by NotificationProcessor on MY_(LARGE)_SEGMENTS_UPDATE notifications
89
+ *
90
+ * @param changeNumber change number of the notification
91
+ * @param segmentsData data for KeyList or SegmentRemoval instant updates
92
+ * @param delay optional time to wait for BoundedFetchRequest or BoundedFetchRequest updates
93
+ */
94
+ put(mySegmentsData: Pick<MySegmentsData, 'type' | 'cn'>, payload?: Pick<MySegmentsData, 'added' | 'removed'>, delay?: number) {
95
+ const { type, cn } = mySegmentsData;
96
+ // Discard event if it is outdated or there is a pending fetch request (_delay is set), but update target change number
97
+ if (cn <= Math.max(currentChangeNumber, mySegmentsCache.getChangeNumber()) || cn <= maxChangeNumber) return;
98
+ maxChangeNumber = cn;
99
+ if (_delay) return;
100
+
101
+ handleNewEvent = true;
102
+ cdnBypass = false;
103
+ _segmentsData = payload && { type, cn, added: payload.added, removed: payload.removed };
104
+ _delay = delay;
105
+
106
+ if (backoff.timeoutID || !isHandlingEvent) __handleMySegmentsUpdateCall();
107
+ backoff.reset();
108
+ },
109
+
110
+ stop() {
111
+ clearTimeout(_delayTimeoutID);
112
+ _delay = undefined;
113
+ isHandlingEvent = false;
114
+ backoff.reset();
115
+ }
116
+ };
41
117
  }
42
118
 
119
+ const updateWorkers = {
120
+ [MEMBERSHIPS_MS_UPDATE]: createUpdateWorker(storage.segments),
121
+ [MEMBERSHIPS_LS_UPDATE]: createUpdateWorker(storage.largeSegments!),
122
+ };
123
+
43
124
  return {
44
- /**
45
- * Invoked by NotificationProcessor on MY_SEGMENTS_UPDATE event
46
- *
47
- * @param {number} changeNumber change number of the MY_SEGMENTS_UPDATE notification
48
- * @param {SegmentsData | undefined} segmentsData might be undefined
49
- */
50
- put(changeNumber: number, segmentsData?: MySegmentsData) {
51
- if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return;
52
-
53
- maxChangeNumber = changeNumber;
54
- handleNewEvent = true;
55
- _segmentsData = segmentsData;
56
-
57
- if (backoff.timeoutID || !isHandlingEvent) __handleMySegmentsUpdateCall();
58
- backoff.reset();
125
+ put(mySegmentsData: Pick<MySegmentsData, 'type' | 'cn'>, payload?: Pick<MySegmentsData, 'added' | 'removed'>, delay?: number) {
126
+ updateWorkers[mySegmentsData.type].put(mySegmentsData, payload, delay);
59
127
  },
60
-
61
128
  stop() {
62
- isHandlingEvent = false;
63
- backoff.reset();
129
+ updateWorkers[MEMBERSHIPS_MS_UPDATE].stop();
130
+ updateWorkers[MEMBERSHIPS_LS_UPDATE].stop();
64
131
  }
65
132
  };
66
133
  }
@@ -9,7 +9,7 @@ import { IUpdateWorker } from './types';
9
9
  /**
10
10
  * SegmentsUpdateWorker factory
11
11
  */
12
- export function SegmentsUpdateWorker(log: ILogger, segmentsSyncTask: ISegmentsSyncTask, segmentsCache: ISegmentsCacheSync): IUpdateWorker {
12
+ export function SegmentsUpdateWorker(log: ILogger, segmentsSyncTask: ISegmentsSyncTask, segmentsCache: ISegmentsCacheSync): IUpdateWorker<[ISegmentUpdateData]> {
13
13
 
14
14
  // Handles retries with CDN bypass per segment name
15
15
  function SegmentUpdateWorker(segment: string) {
@@ -14,7 +14,7 @@ import { IUpdateWorker } from './types';
14
14
  /**
15
15
  * SplitsUpdateWorker factory
16
16
  */
17
- export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker & { killSplit(event: ISplitKillData): void } {
17
+ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData, payload?: ISplit]> & { killSplit(event: ISplitKillData): void } {
18
18
 
19
19
  let maxChangeNumber = 0;
20
20
  let handleNewEvent = false;
@@ -1,4 +1,4 @@
1
- export interface IUpdateWorker {
1
+ export interface IUpdateWorker<T extends any[]> {
2
2
  stop(): void // clear scheduled tasks (backoff)
3
- put(...args: any[]): void // handle new update event
3
+ put(...args: T): void // handle new update event
4
4
  }
@@ -25,8 +25,8 @@ export const PUSH_SUBSYSTEM_UP = 'PUSH_SUBSYSTEM_UP';
25
25
  export const PUSH_SUBSYSTEM_DOWN = 'PUSH_SUBSYSTEM_DOWN';
26
26
 
27
27
  // Update-type push notifications, handled by NotificationProcessor
28
- export const MY_SEGMENTS_UPDATE = 'MY_SEGMENTS_UPDATE';
29
- export const MY_SEGMENTS_UPDATE_V2 = 'MY_SEGMENTS_UPDATE_V2';
28
+ export const MEMBERSHIPS_MS_UPDATE = 'MEMBERSHIPS_MS_UPDATE';
29
+ export const MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE';
30
30
  export const SEGMENT_UPDATE = 'SEGMENT_UPDATE';
31
31
  export const SPLIT_KILL = 'SPLIT_KILL';
32
32
  export const SPLIT_UPDATE = 'SPLIT_UPDATE';
@@ -1,6 +1,8 @@
1
1
  import { algorithms } from '../../utils/decompress';
2
2
  import { decodeFromBase64 } from '../../utils/base64';
3
- import { Compression, KeyList } from './SSEHandler/types';
3
+ import { hash } from '../../utils/murmur3/murmur3';
4
+ import { Compression, IMembershipMSUpdateData, KeyList } from './SSEHandler/types';
5
+ import { ISplit } from '../../dtos/types';
4
6
 
5
7
  const GZIP = 1;
6
8
  const ZLIB = 2;
@@ -42,7 +44,7 @@ function decompress(data: string, compression: Compression) {
42
44
  * @returns {{a?: string[], r?: string[] }}
43
45
  * @throws if data string cannot be decoded, decompressed or parsed
44
46
  */
45
- export function parseKeyList(data: string, compression: Compression, avoidPrecisionLoss: boolean = true): KeyList {
47
+ export function parseKeyList(data: string, compression: Compression, avoidPrecisionLoss = true): KeyList {
46
48
  const binKeyList = decompress(data, compression);
47
49
  let strKeyList = Uint8ArrayToString(binKeyList);
48
50
  // replace numbers to strings, to avoid losing precision
@@ -80,14 +82,20 @@ export function isInBitmap(bitmap: Uint8Array, hash64hex: string) {
80
82
 
81
83
  /**
82
84
  * Parse feature flags notifications for instant feature flag updates
83
- *
84
- * @param {ISplitUpdateData} data
85
- * @returns {KeyList}
86
85
  */
87
- export function parseFFUpdatePayload(compression: Compression, data: string): KeyList | undefined {
88
- const avoidPrecisionLoss = false;
89
- if (compression > 0)
90
- return parseKeyList(data, compression, avoidPrecisionLoss);
91
- else
92
- return JSON.parse(decodeFromBase64(data));
86
+ export function parseFFUpdatePayload(compression: Compression, data: string): ISplit | undefined {
87
+ return compression > 0 ?
88
+ parseKeyList(data, compression, false) :
89
+ JSON.parse(decodeFromBase64(data));
90
+ }
91
+
92
+ const DEFAULT_MAX_INTERVAL = 60000;
93
+
94
+ export function getDelay(parsedData: Pick<IMembershipMSUpdateData, 'i' | 'h' | 's'>, matchingKey: string) {
95
+ if (parsedData.h === 0) return 0;
96
+
97
+ const interval = parsedData.i || DEFAULT_MAX_INTERVAL;
98
+ const seed = parsedData.s || 0;
99
+
100
+ return hash(matchingKey, seed) % interval;
93
101
  }
@@ -11,16 +11,15 @@ import { authenticateFactory, hashUserKey } from './AuthClient';
11
11
  import { forOwn } from '../../utils/lang';
12
12
  import { SSEClient } from './SSEClient';
13
13
  import { getMatching } from '../../utils/key';
14
- import { MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants';
15
- import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, STREAMING_PARSING_SPLIT_UPDATE } from '../../logger/constants';
16
- import { KeyList, UpdateStrategy } from './SSEHandler/types';
17
- import { isInBitmap, parseBitmap, parseFFUpdatePayload, parseKeyList } from './parseUtils';
14
+ import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants';
15
+ import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MEMBERSHIPS_UPDATE, STREAMING_PARSING_SPLIT_UPDATE } from '../../logger/constants';
16
+ import { IMembershipMSUpdateData, IMembershipLSUpdateData, KeyList, UpdateStrategy } from './SSEHandler/types';
17
+ import { getDelay, isInBitmap, parseBitmap, parseFFUpdatePayload, parseKeyList } from './parseUtils';
18
18
  import { ISet, _Set } from '../../utils/lang/sets';
19
19
  import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64';
20
20
  import { IAuthTokenPushEnabled } from './AuthClient/types';
21
21
  import { TOKEN_REFRESH, AUTH_REJECTION } from '../../utils/constants';
22
22
  import { ISdkFactoryContextSync } from '../../sdkFactory/types';
23
- import { IUpdateWorker } from './UpdateWorkers/types';
24
23
 
25
24
  /**
26
25
  * PushManager factory:
@@ -60,11 +59,11 @@ export function pushManagerFactory(
60
59
  // For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments
61
60
  const splitsUpdateWorker = SplitsUpdateWorker(log, storage.splits, pollingManager.splitsSyncTask, readiness.splits, telemetryTracker, userKey ? undefined : pollingManager.segmentsSyncTask as ISegmentsSyncTask);
62
61
 
63
- // [Only for client-side] map of hashes to user keys, to dispatch MY_SEGMENTS_UPDATE events to the corresponding MySegmentsUpdateWorker
62
+ // [Only for client-side] map of hashes to user keys, to dispatch membership update events to the corresponding MySegmentsUpdateWorker
64
63
  const userKeyHashes: Record<string, string> = {};
65
64
  // [Only for client-side] map of user keys to their corresponding hash64 and MySegmentsUpdateWorkers.
66
- // Hash64 is used to process MY_SEGMENTS_UPDATE_V2 events and dispatch actions to the corresponding MySegmentsUpdateWorker.
67
- const clients: Record<string, { hash64: Hash64, worker: IUpdateWorker }> = {};
65
+ // Hash64 is used to process membership update events and dispatch actions to the corresponding MySegmentsUpdateWorker.
66
+ const clients: Record<string, { hash64: Hash64, worker: ReturnType<typeof MySegmentsUpdateWorker> }> = {};
68
67
 
69
68
  // [Only for client-side] variable to flag that a new client was added. It is needed to reconnect streaming.
70
69
  let connectForNewClient = false;
@@ -236,76 +235,75 @@ export function pushManagerFactory(
236
235
  splitsUpdateWorker.put(parsedData);
237
236
  });
238
237
 
239
- if (userKey) {
240
- pushEmitter.on(MY_SEGMENTS_UPDATE, function handleMySegmentsUpdate(parsedData, channel) {
241
- const userKeyHash = channel.split('_')[2];
242
- const userKey = userKeyHashes[userKeyHash];
243
- if (userKey && clients[userKey]) { // check existence since it can be undefined if client has been destroyed
244
- clients[userKey].worker.put(
245
- parsedData.changeNumber,
246
- parsedData.includesPayload ? parsedData.segmentList ? parsedData.segmentList : [] : undefined);
247
- }
248
- });
249
- pushEmitter.on(MY_SEGMENTS_UPDATE_V2, function handleMySegmentsUpdate(parsedData) {
250
- switch (parsedData.u) {
251
- case UpdateStrategy.BoundedFetchRequest: {
252
- let bitmap: Uint8Array;
253
- try {
254
- bitmap = parseBitmap(parsedData.d, parsedData.c);
255
- } catch (e) {
256
- log.warn(STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, ['BoundedFetchRequest', e]);
257
- break;
258
- }
259
-
260
- forOwn(clients, ({ hash64, worker }) => {
261
- if (isInBitmap(bitmap, hash64.hex)) {
262
- worker.put(parsedData.changeNumber); // fetch mySegments
263
- }
264
- });
265
- return;
238
+ function handleMySegmentsUpdate(parsedData: IMembershipMSUpdateData | IMembershipLSUpdateData) {
239
+ switch (parsedData.u) {
240
+ case UpdateStrategy.BoundedFetchRequest: {
241
+ let bitmap: Uint8Array;
242
+ try {
243
+ bitmap = parseBitmap(parsedData.d!, parsedData.c!);
244
+ } catch (e) {
245
+ log.warn(STREAMING_PARSING_MEMBERSHIPS_UPDATE, ['BoundedFetchRequest', e]);
246
+ break;
266
247
  }
267
- case UpdateStrategy.KeyList: {
268
- let keyList: KeyList, added: ISet<string>, removed: ISet<string>;
269
- try {
270
- keyList = parseKeyList(parsedData.d, parsedData.c);
271
- added = new _Set(keyList.a);
272
- removed = new _Set(keyList.r);
273
- } catch (e) {
274
- log.warn(STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, ['KeyList', e]);
275
- break;
248
+
249
+ forOwn(clients, ({ hash64, worker }, matchingKey) => {
250
+ if (isInBitmap(bitmap, hash64.hex)) {
251
+ worker.put(parsedData, undefined, getDelay(parsedData, matchingKey));
276
252
  }
253
+ });
254
+ return;
255
+ }
256
+ case UpdateStrategy.KeyList: {
257
+ let keyList: KeyList, added: ISet<string>, removed: ISet<string>;
258
+ try {
259
+ keyList = parseKeyList(parsedData.d!, parsedData.c!);
260
+ added = new _Set(keyList.a);
261
+ removed = new _Set(keyList.r);
262
+ } catch (e) {
263
+ log.warn(STREAMING_PARSING_MEMBERSHIPS_UPDATE, ['KeyList', e]);
264
+ break;
265
+ }
277
266
 
278
- forOwn(clients, ({ hash64, worker }) => {
279
- const add = added.has(hash64.dec) ? true : removed.has(hash64.dec) ? false : undefined;
280
- if (add !== undefined) {
281
- worker.put(parsedData.changeNumber, {
282
- name: parsedData.segmentName,
283
- add
284
- });
285
- }
286
- });
287
- return;
267
+ if (!parsedData.n || !parsedData.n.length) {
268
+ log.warn(STREAMING_PARSING_MEMBERSHIPS_UPDATE, ['KeyList', 'No segment name was provided']);
269
+ break;
288
270
  }
289
- case UpdateStrategy.SegmentRemoval:
290
- if (!parsedData.segmentName) {
291
- log.warn(STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, ['SegmentRemoval', 'No segment name was provided']);
292
- break;
293
- }
294
271
 
295
- forOwn(clients, ({ worker }) =>
296
- worker.put(parsedData.changeNumber, {
297
- name: parsedData.segmentName,
298
- add: false
299
- })
300
- );
301
- return;
272
+ forOwn(clients, ({ hash64, worker }) => {
273
+ const add = added.has(hash64.dec) ? true : removed.has(hash64.dec) ? false : undefined;
274
+ if (add !== undefined) {
275
+ worker.put(parsedData, {
276
+ added: add ? [parsedData.n![0]] : [],
277
+ removed: add ? [] : [parsedData.n![0]]
278
+ });
279
+ }
280
+ });
281
+ return;
302
282
  }
283
+ case UpdateStrategy.SegmentRemoval:
284
+ if (!parsedData.n || !parsedData.n.length) {
285
+ log.warn(STREAMING_PARSING_MEMBERSHIPS_UPDATE, ['SegmentRemoval', 'No segment name was provided']);
286
+ break;
287
+ }
303
288
 
304
- // `UpdateStrategy.UnboundedFetchRequest` and fallbacks of other cases
305
- forOwn(clients, ({ worker }) => {
306
- worker.put(parsedData.changeNumber);
307
- });
289
+ forOwn(clients, ({ worker }) => {
290
+ worker.put(parsedData, {
291
+ added: [],
292
+ removed: parsedData.n!
293
+ });
294
+ });
295
+ return;
296
+ }
297
+
298
+ // `UpdateStrategy.UnboundedFetchRequest` and fallbacks of other cases
299
+ forOwn(clients, ({ worker }, matchingKey) => {
300
+ worker.put(parsedData, undefined, getDelay(parsedData, matchingKey));
308
301
  });
302
+ }
303
+
304
+ if (userKey) {
305
+ pushEmitter.on(MEMBERSHIPS_MS_UPDATE, handleMySegmentsUpdate);
306
+ pushEmitter.on(MEMBERSHIPS_LS_UPDATE, handleMySegmentsUpdate);
309
307
  } else {
310
308
  pushEmitter.on(SEGMENT_UPDATE, segmentsUpdateWorker!.put);
311
309
  }
@@ -328,7 +326,7 @@ export function pushManagerFactory(
328
326
  if (disabled || disconnected === false) return;
329
327
  disconnected = false;
330
328
 
331
- if (userKey) this.add(userKey, pollingManager.segmentsSyncTask as IMySegmentsSyncTask); // client-side
329
+ if (userKey) this.add(userKey, pollingManager.segmentsSyncTask); // client-side
332
330
  else setTimeout(connectPush); // server-side runs in next cycle as in client-side, for consistency with client-side
333
331
  },
334
332
 
@@ -343,7 +341,10 @@ export function pushManagerFactory(
343
341
 
344
342
  if (!userKeyHashes[hash]) {
345
343
  userKeyHashes[hash] = userKey;
346
- clients[userKey] = { hash64: hash64(userKey), worker: MySegmentsUpdateWorker(mySegmentsSyncTask, telemetryTracker) };
344
+ clients[userKey] = {
345
+ hash64: hash64(userKey),
346
+ worker: MySegmentsUpdateWorker(log, storage, mySegmentsSyncTask, telemetryTracker)
347
+ };
347
348
  connectForNewClient = true; // we must reconnect on start, to listen the channel for the new user key
348
349
 
349
350
  // Reconnects in case of a new client.
@@ -1,4 +1,4 @@
1
- import { IMySegmentsUpdateData, IMySegmentsUpdateV2Data, ISegmentUpdateData, ISplitUpdateData, ISplitKillData } from './SSEHandler/types';
1
+ import { IMembershipMSUpdateData, IMembershipLSUpdateData, ISegmentUpdateData, ISplitUpdateData, ISplitKillData, INotificationData } from './SSEHandler/types';
2
2
  import { ITask } from '../types';
3
3
  import { IMySegmentsSyncTask } from '../polling/types';
4
4
  import { IEventEmitter } from '../../types';
@@ -11,8 +11,8 @@ export type PUSH_NONRETRYABLE_ERROR = 'PUSH_NONRETRYABLE_ERROR'
11
11
  export type PUSH_RETRYABLE_ERROR = 'PUSH_RETRYABLE_ERROR'
12
12
 
13
13
  // Update-type push notifications, handled by NotificationProcessor
14
- export type MY_SEGMENTS_UPDATE = 'MY_SEGMENTS_UPDATE';
15
- export type MY_SEGMENTS_UPDATE_V2 = 'MY_SEGMENTS_UPDATE_V2';
14
+ export type MEMBERSHIPS_MS_UPDATE = 'MEMBERSHIPS_MS_UPDATE';
15
+ export type MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE';
16
16
  export type SEGMENT_UPDATE = 'SEGMENT_UPDATE';
17
17
  export type SPLIT_KILL = 'SPLIT_KILL';
18
18
  export type SPLIT_UPDATE = 'SPLIT_UPDATE';
@@ -21,23 +21,23 @@ export type SPLIT_UPDATE = 'SPLIT_UPDATE';
21
21
  export type CONTROL = 'CONTROL';
22
22
  export type OCCUPANCY = 'OCCUPANCY';
23
23
 
24
- export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MY_SEGMENTS_UPDATE | MY_SEGMENTS_UPDATE_V2 | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | ControlType.STREAMING_RESET
24
+ export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | ControlType.STREAMING_RESET
25
25
 
26
26
  type IParsedData<T extends IPushEvent> =
27
- T extends MY_SEGMENTS_UPDATE ? IMySegmentsUpdateData :
28
- T extends MY_SEGMENTS_UPDATE_V2 ? IMySegmentsUpdateV2Data :
27
+ T extends MEMBERSHIPS_MS_UPDATE ? IMembershipMSUpdateData :
28
+ T extends MEMBERSHIPS_LS_UPDATE ? IMembershipLSUpdateData :
29
29
  T extends SEGMENT_UPDATE ? ISegmentUpdateData :
30
30
  T extends SPLIT_UPDATE ? ISplitUpdateData :
31
- T extends SPLIT_KILL ? ISplitKillData : undefined;
31
+ T extends SPLIT_KILL ? ISplitKillData : INotificationData;
32
32
 
33
33
  /**
34
34
  * EventEmitter used as Feedback Loop between the SyncManager and PushManager,
35
35
  * where the latter pushes messages and the former consumes it
36
36
  */
37
37
  export interface IPushEventEmitter extends IEventEmitter {
38
- once<T extends IPushEvent>(event: T, listener: (parsedData: IParsedData<T>, channel: T extends MY_SEGMENTS_UPDATE ? string : undefined) => void): this;
39
- on<T extends IPushEvent>(event: T, listener: (parsedData: IParsedData<T>, channel: T extends MY_SEGMENTS_UPDATE ? string : undefined) => void): this;
40
- emit<T extends IPushEvent>(event: T, parsedData?: IParsedData<T>, channel?: T extends MY_SEGMENTS_UPDATE ? string : undefined): boolean;
38
+ once<T extends IPushEvent>(event: T, listener: (parsedData: IParsedData<T>) => void): this;
39
+ on<T extends IPushEvent>(event: T, listener: (parsedData: IParsedData<T>) => void): this;
40
+ emit<T extends IPushEvent>(event: T, parsedData?: IParsedData<T>): boolean;
41
41
  }
42
42
 
43
43
  /**
@@ -103,7 +103,7 @@ export type DROPPED = 1;
103
103
  export type DEDUPED = 2;
104
104
  export type ImpressionDataType = QUEUED | DROPPED | DEDUPED
105
105
  export type EventDataType = QUEUED | DROPPED;
106
- export type UpdatesFromSSEEnum = SPLITS | MY_SEGMENT;
106
+ export type UpdatesFromSSEEnum = SPLITS | MEMBERSHIPS;
107
107
 
108
108
  export type SPLITS = 'sp';
109
109
  export type IMPRESSIONS = 'im';
@@ -112,8 +112,8 @@ export type EVENTS = 'ev';
112
112
  export type TELEMETRY = 'te';
113
113
  export type TOKEN = 'to';
114
114
  export type SEGMENT = 'se';
115
- export type MY_SEGMENT = 'ms';
116
- export type OperationType = SPLITS | IMPRESSIONS | IMPRESSIONS_COUNT | EVENTS | TELEMETRY | TOKEN | SEGMENT | MY_SEGMENT;
115
+ export type MEMBERSHIPS = 'ms';
116
+ export type OperationType = SPLITS | IMPRESSIONS | IMPRESSIONS_COUNT | EVENTS | TELEMETRY | TOKEN | SEGMENT | MEMBERSHIPS;
117
117
 
118
118
  export type LastSync = Partial<Record<OperationType, number | undefined>>
119
119
  export type HttpErrors = Partial<Record<OperationType, { [statusCode: string]: number }>>
@@ -158,8 +158,9 @@ export type TelemetryUsageStats = {
158
158
 
159
159
  // amount of instant updates that we are doing by avoiding fetching to Split servers
160
160
  export type UpdatesFromSSE = {
161
- sp: number, // splits
161
+ sp?: number, // splits
162
162
  ms?: number, // my segments
163
+ mls?: number // my large segments
163
164
  }
164
165
 
165
166
  // 'metrics/usage' JSON request body
@@ -175,12 +176,14 @@ export type TelemetryUsageStatsPayload = TelemetryUsageStats & {
175
176
  spC?: number, // splitCount
176
177
  seC?: number, // segmentCount
177
178
  skC?: number, // segmentKeyCount
179
+ lsC?: number, // largeSegmentCount
180
+ lskC?: number, // largeSegmentKeyCount
178
181
  sL?: number, // sessionLengthMs
179
182
  eQ: number, // eventsQueued
180
183
  eD: number, // eventsDropped
181
184
  sE: Array<StreamingEvent>, // streamingEvents
182
185
  t?: Array<string>, // tags
183
- ufs?: UpdatesFromSSE, //UpdatesFromSSE
186
+ ufs?: UpdatesFromSSE, // instant updates
184
187
  }
185
188
 
186
189
  /**
@@ -75,7 +75,7 @@ export const EVENTS = 'ev';
75
75
  export const TELEMETRY = 'te';
76
76
  export const TOKEN = 'to';
77
77
  export const SEGMENT = 'se';
78
- export const MY_SEGMENT = 'ms';
78
+ export const MEMBERSHIPS = 'ms';
79
79
 
80
80
  export const TREATMENT = 't';
81
81
  export const TREATMENTS = 'ts';
@@ -105,7 +105,8 @@ export const DISABLED = 0;
105
105
  export const ENABLED = 1;
106
106
  export const PAUSED = 2;
107
107
 
108
- export const FLAG_SPEC_VERSION = '1.1';
108
+ export const FLAG_SPEC_VERSION = '1.2';
109
109
 
110
110
  // Matcher types
111
111
  export const IN_SEGMENT = 'IN_SEGMENT';
112
+ export const IN_LARGE_SEGMENT = 'IN_LARGE_SEGMENT';
@@ -209,11 +209,12 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
209
209
  const splitFiltersValidation = validateSplitFilters(log, sync.splitFilters, withDefaults.mode);
210
210
  sync.splitFilters = splitFiltersValidation.validFilters;
211
211
  sync.__splitFiltersValidation = splitFiltersValidation;
212
- sync.flagSpecVersion = flagSpec ? flagSpec(withDefaults) : FLAG_SPEC_VERSION;
213
212
 
213
+ // ensure a valid flag spec version
214
+ sync.flagSpecVersion = flagSpec ? flagSpec(withDefaults) : FLAG_SPEC_VERSION;
214
215
  // ensure a valid user consent value
215
216
  // @ts-ignore, modify readonly prop
216
- withDefaults.userConsent = consent(withDefaults);
217
+ withDefaults.userConsent = consent ? consent(withDefaults) : undefined;
217
218
 
218
219
  return withDefaults;
219
220
  }
@@ -25,7 +25,7 @@ export interface ISettingsValidationParams {
25
25
  /** Localhost mode validator (`settings.sync.localhostMode`) */
26
26
  localhost?: (settings: ISettings) => ISettings['sync']['localhostMode'],
27
27
  /** User consent validator (`settings.userConsent`) */
28
- consent: (settings: ISettings) => ISettings['userConsent'],
28
+ consent?: (settings: ISettings) => ISettings['userConsent'],
29
29
  /** Flag spec version validation. Configurable by the JS Synchronizer but not by the SDKs */
30
30
  flagSpec?: (settings: ISettings) => ISettings['sync']['flagSpecVersion']
31
31
  }