@splitsoftware/splitio-commons 2.3.0 → 2.3.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 (157) hide show
  1. package/CHANGES.txt +4 -0
  2. package/README.md +1 -0
  3. package/cjs/evaluator/Engine.js +40 -61
  4. package/cjs/evaluator/combiners/and.js +2 -6
  5. package/cjs/evaluator/combiners/ifelseif.js +6 -6
  6. package/cjs/evaluator/condition/index.js +6 -5
  7. package/cjs/evaluator/index.js +11 -11
  8. package/cjs/evaluator/matchers/index.js +3 -1
  9. package/cjs/evaluator/matchers/matcherTypes.js +1 -0
  10. package/cjs/evaluator/matchers/prerequisites.js +22 -0
  11. package/cjs/evaluator/matchers/rbsegment.js +56 -0
  12. package/cjs/evaluator/matchersTransform/index.js +4 -0
  13. package/cjs/evaluator/parser/index.js +2 -2
  14. package/cjs/evaluator/value/sanitize.js +1 -0
  15. package/cjs/logger/constants.js +5 -3
  16. package/cjs/logger/messages/debug.js +4 -2
  17. package/cjs/logger/messages/warn.js +1 -1
  18. package/cjs/sdkManager/index.js +2 -1
  19. package/cjs/services/splitApi.js +3 -4
  20. package/cjs/services/splitHttpClient.js +1 -1
  21. package/cjs/storages/AbstractSplitsCacheSync.js +5 -2
  22. package/cjs/storages/KeyBuilder.js +9 -0
  23. package/cjs/storages/KeyBuilderCS.js +3 -0
  24. package/cjs/storages/KeyBuilderSS.js +3 -0
  25. package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +117 -0
  26. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +9 -14
  27. package/cjs/storages/inLocalStorage/index.js +5 -1
  28. package/cjs/storages/inLocalStorage/validateCache.js +2 -1
  29. package/cjs/storages/inMemory/InMemoryStorage.js +3 -0
  30. package/cjs/storages/inMemory/InMemoryStorageCS.js +4 -0
  31. package/cjs/storages/inMemory/RBSegmentsCacheInMemory.js +61 -0
  32. package/cjs/storages/inMemory/SplitsCacheInMemory.js +1 -0
  33. package/cjs/storages/inRedis/RBSegmentsCacheInRedis.js +64 -0
  34. package/cjs/storages/inRedis/index.js +2 -0
  35. package/cjs/storages/pluggable/RBSegmentsCachePluggable.js +64 -0
  36. package/cjs/storages/pluggable/index.js +2 -0
  37. package/cjs/sync/polling/fetchers/splitChangesFetcher.js +54 -4
  38. package/cjs/sync/polling/pollingManagerCS.js +7 -7
  39. package/cjs/sync/polling/syncTasks/splitsSyncTask.js +1 -1
  40. package/cjs/sync/polling/updaters/mySegmentsUpdater.js +2 -2
  41. package/cjs/sync/polling/updaters/segmentChangesUpdater.js +1 -1
  42. package/cjs/sync/polling/updaters/splitChangesUpdater.js +59 -33
  43. package/cjs/sync/streaming/SSEHandler/index.js +1 -0
  44. package/cjs/sync/streaming/UpdateWorkers/SplitsUpdateWorker.js +106 -77
  45. package/cjs/sync/streaming/constants.js +2 -1
  46. package/cjs/sync/streaming/pushManager.js +3 -16
  47. package/cjs/sync/syncManagerOnline.js +2 -2
  48. package/cjs/utils/constants/index.js +6 -2
  49. package/cjs/utils/labels/index.js +2 -1
  50. package/esm/evaluator/Engine.js +40 -62
  51. package/esm/evaluator/combiners/and.js +2 -6
  52. package/esm/evaluator/combiners/ifelseif.js +7 -7
  53. package/esm/evaluator/condition/index.js +6 -5
  54. package/esm/evaluator/index.js +12 -12
  55. package/esm/evaluator/matchers/index.js +3 -1
  56. package/esm/evaluator/matchers/matcherTypes.js +1 -0
  57. package/esm/evaluator/matchers/prerequisites.js +18 -0
  58. package/esm/evaluator/matchers/rbsegment.js +52 -0
  59. package/esm/evaluator/matchersTransform/index.js +4 -0
  60. package/esm/evaluator/parser/index.js +2 -2
  61. package/esm/evaluator/value/sanitize.js +1 -0
  62. package/esm/logger/constants.js +2 -0
  63. package/esm/logger/messages/debug.js +4 -2
  64. package/esm/logger/messages/warn.js +1 -1
  65. package/esm/sdkManager/index.js +2 -1
  66. package/esm/services/splitApi.js +3 -4
  67. package/esm/services/splitHttpClient.js +1 -1
  68. package/esm/storages/AbstractSplitsCacheSync.js +5 -2
  69. package/esm/storages/KeyBuilder.js +9 -0
  70. package/esm/storages/KeyBuilderCS.js +3 -0
  71. package/esm/storages/KeyBuilderSS.js +3 -0
  72. package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +114 -0
  73. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +9 -14
  74. package/esm/storages/inLocalStorage/index.js +5 -1
  75. package/esm/storages/inLocalStorage/validateCache.js +2 -1
  76. package/esm/storages/inMemory/InMemoryStorage.js +3 -0
  77. package/esm/storages/inMemory/InMemoryStorageCS.js +4 -0
  78. package/esm/storages/inMemory/RBSegmentsCacheInMemory.js +58 -0
  79. package/esm/storages/inMemory/SplitsCacheInMemory.js +1 -0
  80. package/esm/storages/inRedis/RBSegmentsCacheInRedis.js +61 -0
  81. package/esm/storages/inRedis/index.js +2 -0
  82. package/esm/storages/pluggable/RBSegmentsCachePluggable.js +61 -0
  83. package/esm/storages/pluggable/index.js +2 -0
  84. package/esm/sync/polling/fetchers/splitChangesFetcher.js +54 -4
  85. package/esm/sync/polling/pollingManagerCS.js +7 -7
  86. package/esm/sync/polling/syncTasks/splitsSyncTask.js +1 -1
  87. package/esm/sync/polling/updaters/mySegmentsUpdater.js +2 -2
  88. package/esm/sync/polling/updaters/segmentChangesUpdater.js +1 -1
  89. package/esm/sync/polling/updaters/splitChangesUpdater.js +59 -33
  90. package/esm/sync/streaming/SSEHandler/index.js +2 -1
  91. package/esm/sync/streaming/UpdateWorkers/SplitsUpdateWorker.js +102 -73
  92. package/esm/sync/streaming/constants.js +1 -0
  93. package/esm/sync/streaming/pushManager.js +6 -19
  94. package/esm/sync/syncManagerOnline.js +2 -2
  95. package/esm/utils/constants/index.js +5 -1
  96. package/esm/utils/labels/index.js +1 -0
  97. package/package.json +1 -1
  98. package/src/dtos/types.ts +41 -8
  99. package/src/evaluator/Engine.ts +42 -69
  100. package/src/evaluator/combiners/and.ts +5 -4
  101. package/src/evaluator/combiners/ifelseif.ts +7 -9
  102. package/src/evaluator/condition/engineUtils.ts +1 -1
  103. package/src/evaluator/condition/index.ts +12 -12
  104. package/src/evaluator/index.ts +12 -14
  105. package/src/evaluator/matchers/index.ts +3 -1
  106. package/src/evaluator/matchers/matcherTypes.ts +1 -0
  107. package/src/evaluator/matchers/prerequisites.ts +24 -0
  108. package/src/evaluator/matchers/rbsegment.ts +74 -0
  109. package/src/evaluator/matchersTransform/index.ts +3 -0
  110. package/src/evaluator/parser/index.ts +3 -3
  111. package/src/evaluator/types.ts +2 -2
  112. package/src/evaluator/value/index.ts +2 -2
  113. package/src/evaluator/value/sanitize.ts +5 -4
  114. package/src/logger/constants.ts +2 -0
  115. package/src/logger/messages/debug.ts +4 -2
  116. package/src/logger/messages/warn.ts +1 -1
  117. package/src/sdkManager/index.ts +3 -2
  118. package/src/services/splitApi.ts +3 -4
  119. package/src/services/splitHttpClient.ts +1 -1
  120. package/src/services/types.ts +1 -1
  121. package/src/storages/AbstractSplitsCacheSync.ts +6 -3
  122. package/src/storages/KeyBuilder.ts +12 -0
  123. package/src/storages/KeyBuilderCS.ts +4 -0
  124. package/src/storages/KeyBuilderSS.ts +4 -0
  125. package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +136 -0
  126. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +10 -14
  127. package/src/storages/inLocalStorage/index.ts +5 -1
  128. package/src/storages/inLocalStorage/validateCache.ts +3 -1
  129. package/src/storages/inMemory/InMemoryStorage.ts +3 -0
  130. package/src/storages/inMemory/InMemoryStorageCS.ts +4 -0
  131. package/src/storages/inMemory/RBSegmentsCacheInMemory.ts +68 -0
  132. package/src/storages/inMemory/SplitsCacheInMemory.ts +1 -0
  133. package/src/storages/inRedis/RBSegmentsCacheInRedis.ts +79 -0
  134. package/src/storages/inRedis/index.ts +2 -0
  135. package/src/storages/pluggable/RBSegmentsCachePluggable.ts +76 -0
  136. package/src/storages/pluggable/index.ts +2 -0
  137. package/src/storages/types.ts +33 -1
  138. package/src/sync/polling/fetchers/splitChangesFetcher.ts +65 -4
  139. package/src/sync/polling/fetchers/types.ts +1 -0
  140. package/src/sync/polling/pollingManagerCS.ts +7 -7
  141. package/src/sync/polling/syncTasks/splitsSyncTask.ts +1 -1
  142. package/src/sync/polling/types.ts +2 -2
  143. package/src/sync/polling/updaters/mySegmentsUpdater.ts +2 -2
  144. package/src/sync/polling/updaters/segmentChangesUpdater.ts +1 -1
  145. package/src/sync/polling/updaters/splitChangesUpdater.ts +70 -43
  146. package/src/sync/streaming/SSEHandler/index.ts +2 -1
  147. package/src/sync/streaming/SSEHandler/types.ts +2 -2
  148. package/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +98 -68
  149. package/src/sync/streaming/constants.ts +1 -0
  150. package/src/sync/streaming/parseUtils.ts +2 -2
  151. package/src/sync/streaming/pushManager.ts +6 -18
  152. package/src/sync/streaming/types.ts +3 -2
  153. package/src/sync/syncManagerOnline.ts +2 -2
  154. package/src/utils/constants/index.ts +6 -1
  155. package/src/utils/labels/index.ts +1 -0
  156. package/src/utils/lang/index.ts +1 -1
  157. package/types/splitio.d.ts +5 -1
@@ -1,24 +1,85 @@
1
+ import { ISettings } from '../../../types';
2
+ import { ISplitChangesResponse } from '../../../dtos/types';
1
3
  import { IFetchSplitChanges, IResponse } from '../../../services/types';
4
+ import { IStorageBase } from '../../../storages/types';
5
+ import { FLAG_SPEC_VERSION } from '../../../utils/constants';
6
+ import { base } from '../../../utils/settingsValidation';
2
7
  import { ISplitChangesFetcher } from './types';
8
+ import { LOG_PREFIX_SYNC_SPLITS } from '../../../logger/constants';
9
+
10
+ const PROXY_CHECK_INTERVAL_MILLIS_CS = 60 * 60 * 1000; // 1 hour in Client Side
11
+ const PROXY_CHECK_INTERVAL_MILLIS_SS = 24 * PROXY_CHECK_INTERVAL_MILLIS_CS; // 24 hours in Server Side
12
+
13
+ function sdkEndpointOverridden(settings: ISettings) {
14
+ return settings.urls.sdk !== base.urls.sdk;
15
+ }
3
16
 
4
17
  /**
5
18
  * Factory of SplitChanges fetcher.
6
19
  * SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors.
7
20
  */
8
- export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges): ISplitChangesFetcher {
21
+ // @TODO breaking: drop support for Split Proxy below v5.10.0 and simplify the implementation
22
+ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick<IStorageBase, 'splits' | 'rbSegments'>): ISplitChangesFetcher {
23
+
24
+ const log = settings.log;
25
+ const PROXY_CHECK_INTERVAL_MILLIS = settings.core.key !== undefined ? PROXY_CHECK_INTERVAL_MILLIS_CS : PROXY_CHECK_INTERVAL_MILLIS_SS;
26
+ let lastProxyCheckTimestamp: number | undefined;
9
27
 
10
28
  return function splitChangesFetcher(
11
29
  since: number,
12
30
  noCache?: boolean,
13
31
  till?: number,
32
+ rbSince?: number,
14
33
  // Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker
15
34
  decorator?: (promise: Promise<IResponse>) => Promise<IResponse>
16
- ) {
35
+ ): Promise<ISplitChangesResponse> {
36
+
37
+ // Recheck proxy
38
+ if (lastProxyCheckTimestamp && (Date.now() - lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) {
39
+ settings.sync.flagSpecVersion = FLAG_SPEC_VERSION;
40
+ }
41
+
42
+ let splitsPromise = fetchSplitChanges(since, noCache, till, settings.sync.flagSpecVersion === FLAG_SPEC_VERSION ? rbSince : undefined)
43
+ // Handle proxy error with spec 1.3
44
+ .catch((err) => {
45
+ if (err.statusCode === 400 && sdkEndpointOverridden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) {
46
+ log.error(LOG_PREFIX_SYNC_SPLITS + 'Proxy error detected. Retrying with spec 1.2. If you are using Split Proxy, please upgrade to latest version');
47
+ lastProxyCheckTimestamp = Date.now();
48
+ settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec
49
+ return fetchSplitChanges(since, noCache, till); // retry request without rbSince
50
+ }
51
+ throw err;
52
+ });
17
53
 
18
- let splitsPromise = fetchSplitChanges(since, noCache, till);
19
54
  if (decorator) splitsPromise = decorator(splitsPromise);
20
55
 
21
- return splitsPromise.then(resp => resp.json());
56
+ return splitsPromise
57
+ .then(resp => resp.json())
58
+ .then(data => {
59
+ // Using flag spec version 1.2 or below
60
+ if (data.splits) {
61
+ return {
62
+ ff: {
63
+ d: data.splits,
64
+ s: data.since,
65
+ t: data.till
66
+ }
67
+ };
68
+ }
69
+
70
+ // Proxy recovery
71
+ if (lastProxyCheckTimestamp) {
72
+ log.info(LOG_PREFIX_SYNC_SPLITS + 'Proxy error recovered');
73
+ lastProxyCheckTimestamp = undefined;
74
+ return splitChangesFetcher(-1, undefined, undefined, -1)
75
+ .then((splitChangesResponse: ISplitChangesResponse) =>
76
+ Promise.all([storage.splits.clear(), storage.rbSegments.clear()])
77
+ .then(() => splitChangesResponse)
78
+ );
79
+ }
80
+
81
+ return data;
82
+ });
22
83
  };
23
84
 
24
85
  }
@@ -5,6 +5,7 @@ export type ISplitChangesFetcher = (
5
5
  since: number,
6
6
  noCache?: boolean,
7
7
  till?: number,
8
+ rbSince?: number,
8
9
  decorator?: (promise: Promise<IResponse>) => Promise<IResponse>
9
10
  ) => Promise<ISplitChangesResponse>
10
11
 
@@ -43,10 +43,10 @@ export function pollingManagerCSFactory(
43
43
  // smart pausing
44
44
  readiness.splits.on(SDK_SPLITS_ARRIVED, () => {
45
45
  if (!splitsSyncTask.isRunning()) return; // noop if not doing polling
46
- const splitsHaveSegments = storage.splits.usesSegments();
47
- if (splitsHaveSegments !== mySegmentsSyncTask.isRunning()) {
48
- log.info(POLLING_SMART_PAUSING, [splitsHaveSegments ? 'ON' : 'OFF']);
49
- if (splitsHaveSegments) {
46
+ const usingSegments = storage.splits.usesSegments() || storage.rbSegments.usesSegments();
47
+ if (usingSegments !== mySegmentsSyncTask.isRunning()) {
48
+ log.info(POLLING_SMART_PAUSING, [usingSegments ? 'ON' : 'OFF']);
49
+ if (usingSegments) {
50
50
  startMySegmentsSyncTasks();
51
51
  } else {
52
52
  stopMySegmentsSyncTasks();
@@ -59,9 +59,9 @@ export function pollingManagerCSFactory(
59
59
 
60
60
  // smart ready
61
61
  function smartReady() {
62
- if (!readiness.isReady() && !storage.splits.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
62
+ if (!readiness.isReady() && !storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
63
63
  }
64
- if (!storage.splits.usesSegments()) setTimeout(smartReady, 0);
64
+ if (!storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) setTimeout(smartReady, 0);
65
65
  else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady);
66
66
 
67
67
  mySegmentsSyncTasks[matchingKey] = mySegmentsSyncTask;
@@ -77,7 +77,7 @@ export function pollingManagerCSFactory(
77
77
  log.info(POLLING_START);
78
78
 
79
79
  splitsSyncTask.start();
80
- if (storage.splits.usesSegments()) startMySegmentsSyncTasks();
80
+ if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) startMySegmentsSyncTasks();
81
81
  },
82
82
 
83
83
  // Stop periodic fetching (polling)
@@ -21,7 +21,7 @@ export function splitsSyncTaskFactory(
21
21
  settings.log,
22
22
  splitChangesUpdaterFactory(
23
23
  settings.log,
24
- splitChangesFetcherFactory(fetchSplitChanges),
24
+ splitChangesFetcherFactory(fetchSplitChanges, settings, storage),
25
25
  storage,
26
26
  settings.sync.__splitFiltersValidation,
27
27
  readiness.splits,
@@ -1,10 +1,10 @@
1
- import { ISplit } from '../../dtos/types';
1
+ import { IRBSegment, ISplit } from '../../dtos/types';
2
2
  import { IReadinessManager } from '../../readiness/types';
3
3
  import { IStorageSync } from '../../storages/types';
4
4
  import { MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../streaming/types';
5
5
  import { ITask, ISyncTask } from '../types';
6
6
 
7
- export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }], boolean> { }
7
+ export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }], boolean> { }
8
8
 
9
9
  export interface ISegmentsSyncTask extends ISyncTask<[fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number], boolean> { }
10
10
 
@@ -27,7 +27,7 @@ export function mySegmentsUpdaterFactory(
27
27
  matchingKey: string
28
28
  ): IMySegmentsUpdater {
29
29
 
30
- const { splits, segments, largeSegments } = storage;
30
+ const { splits, rbSegments, segments, largeSegments } = storage;
31
31
  let readyOnAlreadyExistentState = true;
32
32
  let startingUp = true;
33
33
 
@@ -51,7 +51,7 @@ export function mySegmentsUpdaterFactory(
51
51
  }
52
52
 
53
53
  // Notify update if required
54
- if (splits.usesSegments() && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
54
+ if ((splits.usesSegments() || rbSegments.usesSegments()) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
55
55
  readyOnAlreadyExistentState = false;
56
56
  segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED);
57
57
  }
@@ -51,7 +51,7 @@ export function segmentChangesUpdaterFactory(
51
51
  * Returned promise will not be rejected.
52
52
  *
53
53
  * @param fetchOnlyNew - if true, only fetch the segments that not exists, i.e., which `changeNumber` is equal to -1.
54
- * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications.
54
+ * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE or RB_SEGMENT_UPDATE notifications.
55
55
  * @param segmentName - segment name to fetch. By passing `undefined` it fetches the list of segments registered at the storage
56
56
  * @param noCache - true to revalidate data to fetch on a SEGMENT_UPDATE notifications.
57
57
  * @param till - till target for the provided segmentName, for CDN bypass.
@@ -1,16 +1,18 @@
1
1
  import { ISegmentsCacheBase, IStorageBase } from '../../../storages/types';
2
2
  import { ISplitChangesFetcher } from '../fetchers/types';
3
- import { ISplit, ISplitChangesResponse, ISplitFiltersValidation } from '../../../dtos/types';
3
+ import { IRBSegment, ISplit, ISplitChangesResponse, ISplitFiltersValidation, MaybeThenable } from '../../../dtos/types';
4
4
  import { ISplitsEventEmitter } from '../../../readiness/types';
5
5
  import { timeout } from '../../../utils/promise/timeout';
6
6
  import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants';
7
7
  import { ILogger } from '../../../logger/types';
8
- import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants';
8
+ import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_RBS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants';
9
9
  import { startsWith } from '../../../utils/lang';
10
- import { IN_SEGMENT } from '../../../utils/constants';
10
+ import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants';
11
11
  import { setToArray } from '../../../utils/lang/sets';
12
+ import { SPLIT_UPDATE } from '../../streaming/constants';
12
13
 
13
- type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) => Promise<boolean>
14
+ export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string };
15
+ type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise<boolean>
14
16
 
15
17
  // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment).
16
18
  // Returns a promise that could be rejected.
@@ -24,27 +26,35 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise<boolean> {
24
26
  }
25
27
 
26
28
  /**
27
- * Collect segments from a raw split definition.
29
+ * Collect segments from a raw FF or RBS definition.
28
30
  * Exported for testing purposes.
29
31
  */
30
- export function parseSegments({ conditions }: ISplit): Set<string> {
31
- let segments = new Set<string>();
32
+ export function parseSegments(ruleEntity: ISplit | IRBSegment, matcherType: typeof IN_SEGMENT | typeof IN_RULE_BASED_SEGMENT = IN_SEGMENT): Set<string> {
33
+ const { conditions = [], excluded } = ruleEntity as IRBSegment;
34
+
35
+ const segments = new Set<string>();
36
+ if (excluded && excluded.segments) {
37
+ excluded.segments.forEach(({ type, name }) => {
38
+ if ((type === STANDARD_SEGMENT && matcherType === IN_SEGMENT) || (type === RULE_BASED_SEGMENT && matcherType === IN_RULE_BASED_SEGMENT)) {
39
+ segments.add(name);
40
+ }
41
+ });
42
+ }
32
43
 
33
44
  for (let i = 0; i < conditions.length; i++) {
34
45
  const matchers = conditions[i].matcherGroup.matchers;
35
46
 
36
47
  matchers.forEach(matcher => {
37
- if (matcher.matcherType === IN_SEGMENT) segments.add(matcher.userDefinedSegmentMatcherData.segmentName);
48
+ if (matcher.matcherType === matcherType) segments.add(matcher.userDefinedSegmentMatcherData.segmentName);
38
49
  });
39
50
  }
40
51
 
41
52
  return segments;
42
53
  }
43
54
 
44
- interface ISplitMutations {
45
- added: ISplit[],
46
- removed: ISplit[],
47
- segments: string[]
55
+ interface ISplitMutations<T extends ISplit | IRBSegment> {
56
+ added: T[],
57
+ removed: T[]
48
58
  }
49
59
 
50
60
  /**
@@ -73,25 +83,21 @@ function matchFilters(featureFlag: ISplit, filters: ISplitFiltersValidation) {
73
83
  * i.e., an object with added splits, removed splits and used segments.
74
84
  * Exported for testing purposes.
75
85
  */
76
- export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersValidation): ISplitMutations {
77
- const segments = new Set<string>();
78
- const computed = entries.reduce((accum, split) => {
79
- if (split.status === 'ACTIVE' && matchFilters(split, filters)) {
80
- accum.added.push(split);
86
+ export function computeMutation<T extends ISplit | IRBSegment>(rules: Array<T>, segments: Set<string>, filters?: ISplitFiltersValidation): ISplitMutations<T> {
87
+
88
+ return rules.reduce((accum, ruleEntity) => {
89
+ if (ruleEntity.status === 'ACTIVE' && (!filters || matchFilters(ruleEntity as ISplit, filters))) {
90
+ accum.added.push(ruleEntity);
81
91
 
82
- parseSegments(split).forEach((segmentName: string) => {
92
+ parseSegments(ruleEntity).forEach((segmentName: string) => {
83
93
  segments.add(segmentName);
84
94
  });
85
95
  } else {
86
- accum.removed.push(split);
96
+ accum.removed.push(ruleEntity);
87
97
  }
88
98
 
89
99
  return accum;
90
- }, { added: [], removed: [], segments: [] } as ISplitMutations);
91
-
92
- computed.segments = setToArray(segments);
93
-
94
- return computed;
100
+ }, { added: [], removed: [] } as ISplitMutations<T>);
95
101
  }
96
102
 
97
103
  /**
@@ -111,14 +117,14 @@ export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersV
111
117
  export function splitChangesUpdaterFactory(
112
118
  log: ILogger,
113
119
  splitChangesFetcher: ISplitChangesFetcher,
114
- storage: Pick<IStorageBase, 'splits' | 'segments'>,
120
+ storage: Pick<IStorageBase, 'splits' | 'rbSegments' | 'segments'>,
115
121
  splitFiltersValidation: ISplitFiltersValidation,
116
122
  splitsEventEmitter?: ISplitsEventEmitter,
117
123
  requestTimeoutBeforeReady: number = 0,
118
124
  retriesOnFailureBeforeReady: number = 0,
119
125
  isClientSide?: boolean
120
- ): ISplitChangesUpdater {
121
- const { splits, segments } = storage;
126
+ ): SplitChangesUpdater {
127
+ const { splits, rbSegments, segments } = storage;
122
128
 
123
129
  let startingUp = true;
124
130
 
@@ -135,32 +141,53 @@ export function splitChangesUpdaterFactory(
135
141
  * @param noCache - true to revalidate data to fetch
136
142
  * @param till - query param to bypass CDN requests
137
143
  */
138
- return function splitChangesUpdater(noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) {
144
+ return function splitChangesUpdater(noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) {
139
145
 
140
146
  /**
141
147
  * @param since - current changeNumber at splitsCache
142
148
  * @param retry - current number of retry attempts
143
149
  */
144
- function _splitChangesUpdater(since: number, retry = 0): Promise<boolean> {
145
- log.debug(SYNC_SPLITS_FETCH, [since]);
146
- return Promise.resolve(splitUpdateNotification ?
147
- { splits: [splitUpdateNotification.payload], till: splitUpdateNotification.changeNumber } :
148
- splitChangesFetcher(since, noCache, till, _promiseDecorator)
150
+ function _splitChangesUpdater(sinces: [number, number], retry = 0): Promise<boolean> {
151
+ const [since, rbSince] = sinces;
152
+ log.debug(SYNC_SPLITS_FETCH, sinces);
153
+ return Promise.resolve(
154
+ instantUpdate ?
155
+ instantUpdate.type === SPLIT_UPDATE ?
156
+ // IFFU edge case: a change to a flag that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet
157
+ Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then((contains) => {
158
+ return contains ?
159
+ { ff: { d: [instantUpdate.payload as ISplit], t: instantUpdate.changeNumber } } :
160
+ splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
161
+ }) :
162
+ { rbs: { d: [instantUpdate.payload as IRBSegment], t: instantUpdate.changeNumber } } :
163
+ splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator)
149
164
  )
150
165
  .then((splitChanges: ISplitChangesResponse) => {
151
166
  startingUp = false;
152
167
 
153
- const mutation = computeSplitsMutation(splitChanges.splits, splitFiltersValidation);
168
+ const usedSegments = new Set<string>();
154
169
 
155
- log.debug(SYNC_SPLITS_UPDATE, [mutation.added.length, mutation.removed.length, mutation.segments.length]);
170
+ let ffUpdate: MaybeThenable<boolean> = false;
171
+ if (splitChanges.ff) {
172
+ const { added, removed } = computeMutation(splitChanges.ff.d, usedSegments, splitFiltersValidation);
173
+ log.debug(SYNC_SPLITS_UPDATE, [added.length, removed.length]);
174
+ ffUpdate = splits.update(added, removed, splitChanges.ff.t);
175
+ }
176
+
177
+ let rbsUpdate: MaybeThenable<boolean> = false;
178
+ if (splitChanges.rbs) {
179
+ const { added, removed } = computeMutation(splitChanges.rbs.d, usedSegments);
180
+ log.debug(SYNC_RBS_UPDATE, [added.length, removed.length]);
181
+ rbsUpdate = rbSegments.update(added, removed, splitChanges.rbs.t);
182
+ }
156
183
 
157
- return Promise.all([
158
- splits.update(mutation.added, mutation.removed, splitChanges.till),
159
- segments.registerSegments(mutation.segments)
160
- ]).then(([isThereUpdate]) => {
184
+ return Promise.all([ffUpdate, rbsUpdate,
185
+ // @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted
186
+ segments.registerSegments(setToArray(usedSegments))
187
+ ]).then(([ffChanged, rbsChanged]) => {
161
188
  if (splitsEventEmitter) {
162
189
  // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
163
- return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && isThereUpdate && (isClientSide || checkAllSegmentsExist(segments))))
190
+ return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
164
191
  .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
165
192
  .then(emitSplitsArrivedEvent => {
166
193
  // emit SDK events
@@ -177,7 +204,7 @@ export function splitChangesUpdaterFactory(
177
204
  if (startingUp && retriesOnFailureBeforeReady > retry) {
178
205
  retry += 1;
179
206
  log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]);
180
- return _splitChangesUpdater(since, retry);
207
+ return _splitChangesUpdater(sinces, retry);
181
208
  } else {
182
209
  startingUp = false;
183
210
  }
@@ -185,7 +212,7 @@ export function splitChangesUpdaterFactory(
185
212
  });
186
213
  }
187
214
 
188
- let sincePromise = Promise.resolve(splits.getChangeNumber()); // `getChangeNumber` never rejects or throws error
189
- return sincePromise.then(_splitChangesUpdater);
215
+ // `getChangeNumber` never rejects or throws error
216
+ return Promise.all([splits.getChangeNumber(), rbSegments.getChangeNumber()]).then(_splitChangesUpdater);
190
217
  };
191
218
  }
@@ -1,6 +1,6 @@
1
1
  import { errorParser, messageParser } from './NotificationParser';
2
2
  import { notificationKeeperFactory } from './NotificationKeeper';
3
- import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE } from '../constants';
3
+ import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RB_SEGMENT_UPDATE } from '../constants';
4
4
  import { IPushEventEmitter } from '../types';
5
5
  import { ISseEventHandler } from '../SSEClient/types';
6
6
  import { INotificationError, INotificationMessage } from './types';
@@ -84,6 +84,7 @@ export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter,
84
84
  case MEMBERSHIPS_MS_UPDATE:
85
85
  case MEMBERSHIPS_LS_UPDATE:
86
86
  case SPLIT_KILL:
87
+ case RB_SEGMENT_UPDATE:
87
88
  pushEmitter.emit(parsedData.type, parsedData);
88
89
  break;
89
90
 
@@ -1,5 +1,5 @@
1
1
  import { ControlType } from '../constants';
2
- import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../types';
2
+ import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE, RB_SEGMENT_UPDATE } from '../types';
3
3
 
4
4
  export enum Compression {
5
5
  None = 0,
@@ -42,7 +42,7 @@ export interface ISegmentUpdateData {
42
42
  }
43
43
 
44
44
  export interface ISplitUpdateData {
45
- type: SPLIT_UPDATE,
45
+ type: SPLIT_UPDATE | RB_SEGMENT_UPDATE,
46
46
  changeNumber: number,
47
47
  pcn?: number,
48
48
  d?: string,
@@ -1,12 +1,16 @@
1
- import { ISplit } from '../../../dtos/types';
1
+ import { IRBSegment, ISplit } from '../../../dtos/types';
2
+ import { STREAMING_PARSING_SPLIT_UPDATE } from '../../../logger/constants';
2
3
  import { ILogger } from '../../../logger/types';
3
4
  import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants';
4
5
  import { ISplitsEventEmitter } from '../../../readiness/types';
5
- import { ISplitsCacheSync } from '../../../storages/types';
6
+ import { IRBSegmentsCacheSync, ISplitsCacheSync, IStorageSync } from '../../../storages/types';
6
7
  import { ITelemetryTracker } from '../../../trackers/types';
7
8
  import { Backoff } from '../../../utils/Backoff';
8
9
  import { SPLITS } from '../../../utils/constants';
9
10
  import { ISegmentsSyncTask, ISplitsSyncTask } from '../../polling/types';
11
+ import { InstantUpdate } from '../../polling/updaters/splitChangesUpdater';
12
+ import { RB_SEGMENT_UPDATE } from '../constants';
13
+ import { parseFFUpdatePayload } from '../parseUtils';
10
14
  import { ISplitKillData, ISplitUpdateData } from '../SSEHandler/types';
11
15
  import { FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT, FETCH_BACKOFF_MAX_RETRIES } from './constants';
12
16
  import { IUpdateWorker } from './types';
@@ -14,102 +18,128 @@ import { IUpdateWorker } from './types';
14
18
  /**
15
19
  * SplitsUpdateWorker factory
16
20
  */
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 } {
21
+ export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData]> & { killSplit(event: ISplitKillData): void } {
18
22
 
19
- let maxChangeNumber = 0;
20
- let handleNewEvent = false;
21
- let isHandlingEvent: boolean;
22
- let cdnBypass: boolean;
23
- let payload: ISplit | undefined;
24
- const backoff = new Backoff(__handleSplitUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT);
23
+ const ff = SplitsUpdateWorker(storage.splits);
24
+ const rbs = SplitsUpdateWorker(storage.rbSegments);
25
25
 
26
- function __handleSplitUpdateCall() {
27
- isHandlingEvent = true;
28
- if (maxChangeNumber > splitsCache.getChangeNumber()) {
29
- handleNewEvent = false;
30
- const splitUpdateNotification = payload ? { payload, changeNumber: maxChangeNumber } : undefined;
31
- // fetch splits revalidating data if cached
32
- splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined, splitUpdateNotification).then(() => {
33
- if (!isHandlingEvent) return; // halt if `stop` has been called
34
- if (handleNewEvent) {
35
- __handleSplitUpdateCall();
36
- } else {
37
- if (splitUpdateNotification) telemetryTracker.trackUpdatesFromSSE(SPLITS);
38
- // fetch new registered segments for server-side API. Not retrying on error
39
- if (segmentsSyncTask) segmentsSyncTask.execute(true);
26
+ function SplitsUpdateWorker(cache: ISplitsCacheSync | IRBSegmentsCacheSync) {
27
+ let maxChangeNumber = -1;
28
+ let handleNewEvent = false;
29
+ let isHandlingEvent: boolean;
30
+ let cdnBypass: boolean;
31
+ let instantUpdate: InstantUpdate | undefined;
32
+ const backoff = new Backoff(__handleSplitUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT);
40
33
 
41
- const attempts = backoff.attempts + 1;
34
+ function __handleSplitUpdateCall() {
35
+ isHandlingEvent = true;
36
+ if (maxChangeNumber > cache.getChangeNumber()) {
37
+ handleNewEvent = false;
38
+ // fetch splits revalidating data if cached
39
+ splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined, instantUpdate).then(() => {
40
+ if (!isHandlingEvent) return; // halt if `stop` has been called
41
+ if (handleNewEvent) {
42
+ __handleSplitUpdateCall();
43
+ } else {
44
+ if (instantUpdate) telemetryTracker.trackUpdatesFromSSE(SPLITS);
45
+ // fetch new registered segments for server-side API. Not retrying on error
46
+ if (segmentsSyncTask) segmentsSyncTask.execute(true);
42
47
 
43
- if (maxChangeNumber <= splitsCache.getChangeNumber()) {
44
- log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`);
45
- isHandlingEvent = false;
46
- return;
47
- }
48
+ const attempts = backoff.attempts + 1;
48
49
 
49
- if (attempts < FETCH_BACKOFF_MAX_RETRIES) {
50
- backoff.scheduleCall();
51
- return;
52
- }
50
+ if (ff.isSync() && rbs.isSync()) {
51
+ log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`);
52
+ isHandlingEvent = false;
53
+ return;
54
+ }
53
55
 
54
- if (cdnBypass) {
55
- log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`);
56
- isHandlingEvent = false;
57
- } else {
58
- backoff.reset();
59
- cdnBypass = true;
60
- __handleSplitUpdateCall();
56
+ if (attempts < FETCH_BACKOFF_MAX_RETRIES) {
57
+ backoff.scheduleCall();
58
+ return;
59
+ }
60
+
61
+ if (cdnBypass) {
62
+ log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`);
63
+ isHandlingEvent = false;
64
+ } else {
65
+ backoff.reset();
66
+ cdnBypass = true;
67
+ __handleSplitUpdateCall();
68
+ }
61
69
  }
62
- }
63
- });
64
- } else {
65
- isHandlingEvent = false;
70
+ });
71
+ } else {
72
+ isHandlingEvent = false;
73
+ }
66
74
  }
67
- }
68
75
 
69
- /**
70
- * Invoked by NotificationProcessor on SPLIT_UPDATE event
71
- *
72
- * @param changeNumber - change number of the SPLIT_UPDATE notification
73
- */
74
- function put({ changeNumber, pcn }: ISplitUpdateData, _payload?: ISplit) {
75
- const currentChangeNumber = splitsCache.getChangeNumber();
76
+ return {
77
+ /**
78
+ * Invoked by NotificationProcessor on SPLIT_UPDATE or RB_SEGMENT_UPDATE event
79
+ *
80
+ * @param changeNumber - change number of the notification
81
+ */
82
+ put({ changeNumber, pcn, type }: ISplitUpdateData, payload?: ISplit | IRBSegment) {
83
+ const currentChangeNumber = cache.getChangeNumber();
76
84
 
77
- if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return;
85
+ if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return;
78
86
 
79
- maxChangeNumber = changeNumber;
80
- handleNewEvent = true;
81
- cdnBypass = false;
82
- payload = undefined;
87
+ maxChangeNumber = changeNumber;
88
+ handleNewEvent = true;
89
+ cdnBypass = false;
90
+ instantUpdate = undefined;
83
91
 
84
- if (_payload && currentChangeNumber === pcn) {
85
- payload = _payload;
86
- }
92
+ if (payload && currentChangeNumber === pcn) {
93
+ instantUpdate = { payload, changeNumber, type };
94
+ }
87
95
 
88
- if (backoff.timeoutID || !isHandlingEvent) __handleSplitUpdateCall();
89
- backoff.reset();
96
+ if (backoff.timeoutID || !isHandlingEvent) __handleSplitUpdateCall();
97
+ backoff.reset();
98
+ },
99
+ stop() {
100
+ isHandlingEvent = false;
101
+ backoff.reset();
102
+ },
103
+ isSync() {
104
+ return maxChangeNumber <= cache.getChangeNumber();
105
+ }
106
+ };
90
107
  }
91
108
 
92
109
  return {
93
- put,
110
+ put(parsedData) {
111
+ if (parsedData.d && parsedData.c !== undefined) {
112
+ try {
113
+ const payload = parseFFUpdatePayload(parsedData.c, parsedData.d);
114
+ if (payload) {
115
+ (parsedData.type === RB_SEGMENT_UPDATE ? rbs : ff).put(parsedData, payload);
116
+ return;
117
+ }
118
+ } catch (e) {
119
+ log.warn(STREAMING_PARSING_SPLIT_UPDATE, [parsedData.type, e]);
120
+ }
121
+ }
122
+ (parsedData.type === RB_SEGMENT_UPDATE ? rbs : ff).put(parsedData);
123
+ },
94
124
  /**
95
125
  * Invoked by NotificationProcessor on SPLIT_KILL event
96
126
  *
97
- * @param changeNumber - change number of the SPLIT_UPDATE notification
127
+ * @param changeNumber - change number of the notification
98
128
  * @param splitName - name of split to kill
99
129
  * @param defaultTreatment - default treatment value
100
130
  */
101
131
  killSplit({ changeNumber, splitName, defaultTreatment }: ISplitKillData) {
102
- if (splitsCache.killLocally(splitName, defaultTreatment, changeNumber)) {
132
+ if (storage.splits.killLocally(splitName, defaultTreatment, changeNumber)) {
103
133
  // trigger an SDK_UPDATE if Split was killed locally
104
134
  splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, true);
105
135
  }
106
136
  // queues the SplitChanges fetch (only if changeNumber is newer)
107
- put({ changeNumber } as ISplitUpdateData);
137
+ ff.put({ changeNumber } as ISplitUpdateData);
108
138
  },
109
139
 
110
140
  stop() {
111
- isHandlingEvent = false;
112
- backoff.reset();
141
+ ff.stop();
142
+ rbs.stop();
113
143
  }
114
144
  };
115
145
  }
@@ -30,6 +30,7 @@ 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';
33
+ export const RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE';
33
34
 
34
35
  // Control-type push notifications, handled by NotificationKeeper
35
36
  export const CONTROL = 'CONTROL';