@splitsoftware/splitio-commons 1.5.1-rc.0 → 1.5.1-rc.3

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 (71) hide show
  1. package/CHANGES.txt +4 -2
  2. package/cjs/integrations/ga/GaToSplit.js +1 -1
  3. package/cjs/services/splitApi.js +4 -4
  4. package/cjs/sync/polling/fetchers/segmentChangesFetcher.js +5 -5
  5. package/cjs/sync/polling/fetchers/splitChangesFetcher.js +2 -2
  6. package/cjs/sync/polling/updaters/segmentChangesUpdater.js +34 -34
  7. package/cjs/sync/polling/updaters/splitChangesUpdater.js +4 -3
  8. package/cjs/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.js +46 -46
  9. package/cjs/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.js +82 -64
  10. package/cjs/sync/streaming/UpdateWorkers/SplitsUpdateWorker.js +74 -58
  11. package/cjs/sync/streaming/UpdateWorkers/constants.js +6 -0
  12. package/cjs/sync/streaming/pushManager.js +6 -7
  13. package/cjs/sync/syncTask.js +13 -16
  14. package/cjs/utils/Backoff.js +3 -2
  15. package/esm/integrations/ga/GaToSplit.js +1 -1
  16. package/esm/services/splitApi.js +4 -4
  17. package/esm/sync/polling/fetchers/segmentChangesFetcher.js +5 -5
  18. package/esm/sync/polling/fetchers/splitChangesFetcher.js +2 -2
  19. package/esm/sync/polling/updaters/segmentChangesUpdater.js +34 -34
  20. package/esm/sync/polling/updaters/splitChangesUpdater.js +4 -3
  21. package/esm/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.js +46 -47
  22. package/esm/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.js +82 -65
  23. package/esm/sync/streaming/UpdateWorkers/SplitsUpdateWorker.js +74 -59
  24. package/esm/sync/streaming/UpdateWorkers/constants.js +3 -0
  25. package/esm/sync/streaming/pushManager.js +6 -7
  26. package/esm/sync/syncTask.js +13 -16
  27. package/esm/utils/Backoff.js +3 -2
  28. package/package.json +1 -5
  29. package/src/integrations/ga/GaToSplit.ts +1 -1
  30. package/src/integrations/ga/autoRequire.js +16 -16
  31. package/src/services/splitApi.ts +4 -4
  32. package/src/services/types.ts +2 -2
  33. package/src/sync/polling/fetchers/segmentChangesFetcher.ts +5 -4
  34. package/src/sync/polling/fetchers/splitChangesFetcher.ts +2 -1
  35. package/src/sync/polling/fetchers/types.ts +2 -0
  36. package/src/sync/polling/pollingManagerCS.ts +5 -5
  37. package/src/sync/polling/syncTasks/mySegmentsSyncTask.ts +2 -2
  38. package/src/sync/polling/types.ts +14 -6
  39. package/src/sync/polling/updaters/mySegmentsUpdater.ts +4 -4
  40. package/src/sync/polling/updaters/segmentChangesUpdater.ts +34 -32
  41. package/src/sync/polling/updaters/splitChangesUpdater.ts +5 -4
  42. package/src/sync/streaming/SSEHandler/types.ts +0 -7
  43. package/src/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.ts +45 -54
  44. package/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts +78 -63
  45. package/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +73 -61
  46. package/src/sync/streaming/UpdateWorkers/constants.ts +3 -0
  47. package/src/sync/streaming/UpdateWorkers/types.ts +2 -4
  48. package/src/sync/streaming/pushManager.ts +12 -12
  49. package/src/sync/streaming/types.ts +2 -2
  50. package/src/sync/syncTask.ts +16 -18
  51. package/src/utils/Backoff.ts +7 -2
  52. package/types/services/types.d.ts +2 -2
  53. package/types/sync/polling/fetchers/types.d.ts +2 -2
  54. package/types/sync/polling/syncTasks/mySegmentsSyncTask.d.ts +2 -2
  55. package/types/sync/polling/types.d.ts +11 -6
  56. package/types/sync/polling/updaters/segmentChangesUpdater.d.ts +1 -1
  57. package/types/sync/polling/updaters/splitChangesUpdater.d.ts +1 -1
  58. package/types/sync/streaming/SSEHandler/types.d.ts +0 -4
  59. package/types/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.d.ts +3 -24
  60. package/types/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.d.ts +3 -23
  61. package/types/sync/streaming/UpdateWorkers/SplitsUpdateWorker.d.ts +6 -33
  62. package/types/sync/streaming/UpdateWorkers/types.d.ts +1 -2
  63. package/types/sync/streaming/types.d.ts +2 -2
  64. package/types/sync/syncTask.d.ts +2 -3
  65. package/types/utils/Backoff.d.ts +2 -0
  66. package/cjs/sync/offline/LocalhostFromFile.js +0 -13
  67. package/cjs/sync/offline/splitsParser/splitsParserFromFile.js +0 -151
  68. package/esm/sync/offline/LocalhostFromFile.js +0 -9
  69. package/esm/sync/offline/splitsParser/splitsParserFromFile.js +0 -146
  70. package/src/sync/offline/LocalhostFromFile.ts +0 -12
  71. package/src/sync/offline/splitsParser/splitsParserFromFile.ts +0 -182
@@ -8,7 +8,7 @@ import { ILogger } from '../../../logger/types';
8
8
  import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants';
9
9
  import { thenable } from '../../../utils/promise/thenable';
10
10
 
11
- type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) => Promise<boolean>
11
+ type ISegmentChangesUpdater = (fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number) => Promise<boolean>
12
12
 
13
13
  /**
14
14
  * Factory of SegmentChanges updater, a task that:
@@ -30,53 +30,56 @@ export function segmentChangesUpdaterFactory(
30
30
 
31
31
  let readyOnAlreadyExistentState = true;
32
32
 
33
+ function updateSegment(segmentName: string, noCache?: boolean, till?: number, fetchOnlyNew?: boolean) {
34
+ log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing segment ${segmentName}`);
35
+ let sincePromise = Promise.resolve(segments.getChangeNumber(segmentName));
36
+
37
+ return sincePromise.then(since => {
38
+ // if fetchOnlyNew flag, avoid processing already fetched segments
39
+ if (fetchOnlyNew && since !== -1) return -1;
40
+
41
+ return segmentChangesFetcher(since, segmentName, noCache, till).then(function (changes) {
42
+ let changeNumber = -1;
43
+ const results: MaybeThenable<boolean | void>[] = [];
44
+ changes.forEach(x => {
45
+ if (x.added.length > 0) results.push(segments.addToSegment(segmentName, x.added));
46
+ if (x.removed.length > 0) results.push(segments.removeFromSegment(segmentName, x.removed));
47
+ if (x.added.length > 0 || x.removed.length > 0) {
48
+ results.push(segments.setChangeNumber(segmentName, x.till));
49
+ changeNumber = x.till;
50
+ }
51
+
52
+ log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`);
53
+ });
54
+ // If at least one storage operation result is a promise, join all in a single promise.
55
+ if (results.some(result => thenable(result))) return Promise.all(results).then(() => changeNumber);
56
+ return changeNumber;
57
+ });
58
+ });
59
+ }
33
60
  /**
34
61
  * Segments updater returns a promise that resolves with a `false` boolean value if it fails at least to fetch a segment or synchronize it with the storage.
35
62
  * Thus, a false result doesn't imply that SDK_SEGMENTS_ARRIVED was not emitted.
36
63
  * Returned promise will not be rejected.
37
64
  *
38
- * @param {string[] | undefined} segmentNames list of segment names to fetch. By passing `undefined` it fetches the list of segments registered at the storage
39
- * @param {boolean | undefined} noCache true to revalidate data to fetch on a SEGMENT_UPDATE notifications.
40
65
  * @param {boolean | undefined} fetchOnlyNew if true, only fetch the segments that not exists, i.e., which `changeNumber` is equal to -1.
41
66
  * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications.
67
+ * @param {string | undefined} segmentName segment name to fetch. By passing `undefined` it fetches the list of segments registered at the storage
68
+ * @param {boolean | undefined} noCache true to revalidate data to fetch on a SEGMENT_UPDATE notifications.
69
+ * @param {number | undefined} till till target for the provided segmentName, for CDN bypass.
42
70
  */
43
- return function segmentChangesUpdater(segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) {
71
+ return function segmentChangesUpdater(fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number) {
44
72
  log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Started segments update`);
45
73
 
46
74
  // If not a segment name provided, read list of available segments names to be updated.
47
- let segmentsPromise = Promise.resolve(segmentNames ? segmentNames : segments.getRegisteredSegments());
75
+ let segmentsPromise = Promise.resolve(segmentName ? [segmentName] : segments.getRegisteredSegments());
48
76
 
49
77
  return segmentsPromise.then(segmentNames => {
50
78
  // Async fetchers are collected here.
51
79
  const updaters: Promise<number>[] = [];
52
80
 
53
81
  for (let index = 0; index < segmentNames.length; index++) {
54
- const segmentName = segmentNames[index];
55
- log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing segment ${segmentName}`);
56
- let sincePromise = Promise.resolve(segments.getChangeNumber(segmentName));
57
-
58
- updaters.push(sincePromise.then(since => {
59
- // if fetchOnlyNew flag, avoid processing already fetched segments
60
- if (fetchOnlyNew && since !== -1) return -1;
61
-
62
- return segmentChangesFetcher(since, segmentName, noCache).then(function (changes) {
63
- let changeNumber = -1;
64
- const results: MaybeThenable<boolean | void>[] = [];
65
- changes.forEach(x => {
66
- if (x.added.length > 0) results.push(segments.addToSegment(segmentName, x.added));
67
- if (x.removed.length > 0) results.push(segments.removeFromSegment(segmentName, x.removed));
68
- if (x.added.length > 0 || x.removed.length > 0) {
69
- results.push(segments.setChangeNumber(segmentName, x.till));
70
- changeNumber = x.till;
71
- }
72
-
73
- log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`);
74
- });
75
- // If at least one storage operation result is a promise, join all in a single promise.
76
- if (results.some(result => thenable(result))) return Promise.all(results).then(() => changeNumber);
77
- return changeNumber;
78
- });
79
- }));
82
+ updaters.push(updateSegment(segmentNames[index], noCache, till, fetchOnlyNew));
80
83
 
81
84
  }
82
85
 
@@ -103,5 +106,4 @@ export function segmentChangesUpdaterFactory(
103
106
  return false;
104
107
  });
105
108
  };
106
-
107
109
  }
@@ -8,7 +8,7 @@ import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/
8
8
  import { ILogger } from '../../../logger/types';
9
9
  import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants';
10
10
 
11
- type ISplitChangesUpdater = (noCache?: boolean) => Promise<boolean>
11
+ type ISplitChangesUpdater = (noCache?: boolean, till?: number) => Promise<boolean>
12
12
 
13
13
  // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment).
14
14
  // Returns a promise that could be rejected.
@@ -109,17 +109,18 @@ export function splitChangesUpdaterFactory(
109
109
  * Returned promise will not be rejected.
110
110
  *
111
111
  * @param {boolean | undefined} noCache true to revalidate data to fetch
112
+ * @param {boolean | undefined} till query param to bypass CDN requests
112
113
  */
113
- return function splitChangesUpdater(noCache?: boolean) {
114
+ return function splitChangesUpdater(noCache?: boolean, till?: number) {
114
115
 
115
116
  /**
116
117
  * @param {number} since current changeNumber at splitsCache
117
- * @param {number} retry current number of retry attemps
118
+ * @param {number} retry current number of retry attempts
118
119
  */
119
120
  function _splitChangesUpdater(since: number, retry = 0): Promise<boolean> {
120
121
  log.debug(SYNC_SPLITS_FETCH, [since]);
121
122
 
122
- const fetcherPromise = splitChangesFetcher(since, noCache, _promiseDecorator)
123
+ const fetcherPromise = splitChangesFetcher(since, noCache, till, _promiseDecorator)
123
124
  .then((splitChanges: ISplitChangesResponse) => {
124
125
  startingUp = false;
125
126
 
@@ -68,10 +68,3 @@ export interface IOccupancyData {
68
68
  export type INotificationData = IMySegmentsUpdateData | IMySegmentsUpdateV2Data | ISegmentUpdateData | ISplitUpdateData | ISplitKillData | IControlData | IOccupancyData
69
69
  export type INotificationMessage = { parsedData: INotificationData, channel: string, timestamp: number, data: string }
70
70
  export type INotificationError = Event & { parsedData?: any, message?: string }
71
-
72
- export type SegmentsData = string[] | {
73
- /* segment name */
74
- name: string,
75
- /* action: `true` for add, and `false` for delete */
76
- add: boolean
77
- }
@@ -1,71 +1,62 @@
1
- import { ISegmentsSyncTask } from '../../polling/types';
1
+ import { IMySegmentsSyncTask, MySegmentsData } from '../../polling/types';
2
2
  import { Backoff } from '../../../utils/Backoff';
3
3
  import { IUpdateWorker } from './types';
4
- import { SegmentsData } from '../SSEHandler/types';
5
4
 
6
5
  /**
7
- * MySegmentsUpdateWorker class
6
+ * MySegmentsUpdateWorker factory
8
7
  */
9
- export class MySegmentsUpdateWorker implements IUpdateWorker {
8
+ export function MySegmentsUpdateWorker(mySegmentsSyncTask: IMySegmentsSyncTask): IUpdateWorker {
10
9
 
11
- private readonly mySegmentsSyncTask: ISegmentsSyncTask;
12
- private maxChangeNumber: number;
13
- private handleNewEvent: boolean;
14
- private segmentsData?: SegmentsData;
15
- private currentChangeNumber: number;
16
- readonly backoff: Backoff;
10
+ let maxChangeNumber = 0; // keeps the maximum changeNumber among queued events
11
+ let currentChangeNumber = -1;
12
+ let handleNewEvent = false;
13
+ let isHandlingEvent: boolean;
14
+ let _segmentsData: MySegmentsData | undefined; // keeps the segmentsData (if included in notification payload) from the queued event with maximum changeNumber
15
+ const backoff = new Backoff(__handleMySegmentsUpdateCall);
17
16
 
18
- /**
19
- * @param {Object} mySegmentsSyncTask task for syncing mySegments data
20
- */
21
- constructor(mySegmentsSyncTask: ISegmentsSyncTask) {
22
- this.mySegmentsSyncTask = mySegmentsSyncTask;
23
- this.maxChangeNumber = 0; // keeps the maximum changeNumber among queued events
24
- this.handleNewEvent = false;
25
- this.segmentsData = undefined; // keeps the segmentsData (if included in notification payload) from the queued event with maximum changeNumber
26
- this.currentChangeNumber = -1; // @TODO: remove once `/mySegments` endpoint provides the changeNumber
27
- this.put = this.put.bind(this);
28
- this.__handleMySegmentsUpdateCall = this.__handleMySegmentsUpdateCall.bind(this);
29
- this.backoff = new Backoff(this.__handleMySegmentsUpdateCall);
30
- }
31
-
32
- // Private method
33
- // Precondition: this.mySegmentsSyncTask.isSynchronizingMySegments === false
34
- __handleMySegmentsUpdateCall() {
35
- if (this.maxChangeNumber > this.currentChangeNumber) {
36
- this.handleNewEvent = false;
37
- const currentMaxChangeNumber = this.maxChangeNumber;
17
+ function __handleMySegmentsUpdateCall() {
18
+ isHandlingEvent = true;
19
+ if (maxChangeNumber > currentChangeNumber) {
20
+ handleNewEvent = false;
21
+ const currentMaxChangeNumber = maxChangeNumber;
38
22
 
39
23
  // fetch mySegments revalidating data if cached
40
- this.mySegmentsSyncTask.execute(this.segmentsData, true).then((result) => {
24
+ mySegmentsSyncTask.execute(_segmentsData, true).then((result) => {
25
+ if (!isHandlingEvent) return; // halt if `stop` has been called
41
26
  if (result !== false) // Unlike `Splits|SegmentsUpdateWorker`, we cannot use `mySegmentsCache.getChangeNumber` since `/mySegments` endpoint doesn't provide this value.
42
- this.currentChangeNumber = Math.max(this.currentChangeNumber, currentMaxChangeNumber); // use `currentMaxChangeNumber`, in case that `this.maxChangeNumber` was updated during fetch.
43
- if (this.handleNewEvent) {
44
- this.__handleMySegmentsUpdateCall();
27
+ currentChangeNumber = Math.max(currentChangeNumber, currentMaxChangeNumber); // use `currentMaxChangeNumber`, in case that `maxChangeNumber` was updated during fetch.
28
+ if (handleNewEvent) {
29
+ __handleMySegmentsUpdateCall();
45
30
  } else {
46
- this.backoff.scheduleCall();
31
+ backoff.scheduleCall();
47
32
  }
48
33
  });
34
+ } else {
35
+ isHandlingEvent = false;
49
36
  }
50
37
  }
51
38
 
52
- /**
53
- * Invoked by NotificationProcessor on MY_SEGMENTS_UPDATE event
54
- *
55
- * @param {number} changeNumber change number of the MY_SEGMENTS_UPDATE notification
56
- * @param {SegmentsData | undefined} segmentsData might be undefined
57
- */
58
- put(changeNumber: number, segmentsData?: SegmentsData) {
59
- if (changeNumber <= this.currentChangeNumber || changeNumber <= this.maxChangeNumber) return;
60
-
61
- this.maxChangeNumber = changeNumber;
62
- this.handleNewEvent = true;
63
- this.backoff.reset();
64
- this.segmentsData = segmentsData;
65
-
66
- if (this.mySegmentsSyncTask.isExecuting()) return;
67
-
68
- this.__handleMySegmentsUpdateCall();
69
- }
70
-
39
+ return {
40
+ /**
41
+ * Invoked by NotificationProcessor on MY_SEGMENTS_UPDATE event
42
+ *
43
+ * @param {number} changeNumber change number of the MY_SEGMENTS_UPDATE notification
44
+ * @param {SegmentsData | undefined} segmentsData might be undefined
45
+ */
46
+ put(changeNumber: number, segmentsData?: MySegmentsData) {
47
+ if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return;
48
+
49
+ maxChangeNumber = changeNumber;
50
+ handleNewEvent = true;
51
+ _segmentsData = segmentsData;
52
+
53
+ if (backoff.timeoutID || !isHandlingEvent) __handleMySegmentsUpdateCall();
54
+ backoff.reset();
55
+ },
56
+
57
+ stop() {
58
+ isHandlingEvent = false;
59
+ backoff.reset();
60
+ }
61
+ };
71
62
  }
@@ -1,84 +1,99 @@
1
+ import { ILogger } from '../../../logger/types';
1
2
  import { ISegmentsCacheSync } from '../../../storages/types';
2
3
  import { Backoff } from '../../../utils/Backoff';
3
4
  import { ISegmentsSyncTask } from '../../polling/types';
4
5
  import { ISegmentUpdateData } from '../SSEHandler/types';
6
+ import { FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_RETRIES, FETCH_BACKOFF_MAX_WAIT } from './constants';
5
7
  import { IUpdateWorker } from './types';
6
8
 
7
9
  /**
8
- * SegmentUpdateWorker class
10
+ * SegmentsUpdateWorker factory
9
11
  */
10
- export class SegmentsUpdateWorker implements IUpdateWorker {
12
+ export function SegmentsUpdateWorker(log: ILogger, segmentsSyncTask: ISegmentsSyncTask, segmentsCache: ISegmentsCacheSync): IUpdateWorker {
11
13
 
12
- private readonly segmentsCache: ISegmentsCacheSync;
13
- private readonly segmentsSyncTask: ISegmentsSyncTask;
14
- private readonly maxChangeNumbers: Record<string, number>;
15
- private handleNewEvent: boolean;
16
- readonly backoff: Backoff;
14
+ // Handles retries with CDN bypass per segment name
15
+ function SegmentUpdateWorker(segment: string) {
16
+ let maxChangeNumber = 0;
17
+ let handleNewEvent = false;
18
+ let isHandlingEvent: boolean;
19
+ let cdnBypass: boolean;
20
+ const backoff = new Backoff(__handleSegmentUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT);
17
21
 
18
- /**
19
- * @param {Object} segmentsCache segments data cache
20
- * @param {Object} segmentsSyncTask task for syncing segments data
21
- */
22
- constructor(segmentsSyncTask: ISegmentsSyncTask, segmentsCache: ISegmentsCacheSync) {
23
- this.segmentsCache = segmentsCache;
24
- this.segmentsSyncTask = segmentsSyncTask;
25
- this.maxChangeNumbers = {};
26
- this.handleNewEvent = false;
27
- this.put = this.put.bind(this);
28
- this.__handleSegmentUpdateCall = this.__handleSegmentUpdateCall.bind(this);
29
- this.backoff = new Backoff(this.__handleSegmentUpdateCall);
30
- }
22
+ function __handleSegmentUpdateCall() {
23
+ isHandlingEvent = true;
24
+ if (maxChangeNumber > segmentsCache.getChangeNumber(segment)) {
25
+ handleNewEvent = false;
31
26
 
32
- // Private method
33
- // Precondition: this.segmentsSyncTask.isSynchronizingSegments === false
34
- // Approach similar to MySegmentsUpdateWorker due to differences on Segments notifications and endpoint changeNumbers
35
- __handleSegmentUpdateCall() {
36
- const segmentsToFetch = Object.keys(this.maxChangeNumbers).filter((segmentName) => {
37
- return this.maxChangeNumbers[segmentName] > this.segmentsCache.getChangeNumber(segmentName);
38
- });
39
- if (segmentsToFetch.length > 0) {
40
- this.handleNewEvent = false;
41
- const currentMaxChangeNumbers = segmentsToFetch.map(segmentToFetch => this.maxChangeNumbers[segmentToFetch]);
27
+ // fetch segments revalidating data if cached
28
+ segmentsSyncTask.execute(false, segment, true, cdnBypass ? maxChangeNumber : undefined).then(() => {
29
+ if (!isHandlingEvent) return; // halt if `stop` has been called
30
+ if (handleNewEvent) {
31
+ __handleSegmentUpdateCall();
32
+ } else {
33
+ const attempts = backoff.attempts + 1;
42
34
 
43
- // fetch segments revalidating data if cached
44
- this.segmentsSyncTask.execute(segmentsToFetch, true).then((result) => {
45
- // Unlike `SplitUpdateWorker` where changeNumber is consistent between notification and endpoint, if there is no error,
46
- // we must clean the `maxChangeNumbers` of those segments that didn't receive a new update notification during the fetch.
47
- if (result !== false) {
48
- segmentsToFetch.forEach((fetchedSegment, index) => {
49
- if (this.maxChangeNumbers[fetchedSegment] === currentMaxChangeNumbers[index]) this.maxChangeNumbers[fetchedSegment] = -1;
50
- });
51
- } else {
52
- // recursive invocation with backoff if there was some error
53
- this.backoff.scheduleCall();
54
- }
35
+ if (maxChangeNumber <= segmentsCache.getChangeNumber(segment)) {
36
+ log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`);
37
+ isHandlingEvent = false;
38
+ return;
39
+ }
55
40
 
56
- // immediate recursive invocation if a new notification was queued during fetch
57
- if (this.handleNewEvent) {
58
- this.__handleSegmentUpdateCall();
59
- }
60
- });
61
- }
62
- }
41
+ if (attempts < FETCH_BACKOFF_MAX_RETRIES) {
42
+ backoff.scheduleCall();
43
+ return;
44
+ }
63
45
 
64
- /**
65
- * Invoked by NotificationProcessor on SEGMENT_UPDATE event
66
- *
67
- * @param {number} changeNumber change number of the SEGMENT_UPDATE notification
68
- * @param {string} segmentName segment name of the SEGMENT_UPDATE notification
69
- */
70
- put({ changeNumber, segmentName }: ISegmentUpdateData) {
71
- const currentChangeNumber = this.segmentsCache.getChangeNumber(segmentName);
46
+ if (cdnBypass) {
47
+ log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`);
48
+ isHandlingEvent = false;
49
+ } else {
50
+ backoff.reset();
51
+ cdnBypass = true;
52
+ __handleSegmentUpdateCall();
53
+ }
54
+ }
55
+ });
56
+ } else {
57
+ isHandlingEvent = false;
58
+ }
59
+ }
72
60
 
73
- if (changeNumber <= currentChangeNumber || changeNumber <= this.maxChangeNumbers[segmentName]) return;
61
+ return {
62
+ put(changeNumber: number) {
63
+ const currentChangeNumber = segmentsCache.getChangeNumber(segment);
74
64
 
75
- this.maxChangeNumbers[segmentName] = changeNumber;
76
- this.handleNewEvent = true;
77
- this.backoff.reset();
65
+ if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return;
78
66
 
79
- if (this.segmentsSyncTask.isExecuting()) return;
67
+ maxChangeNumber = changeNumber;
68
+ handleNewEvent = true;
69
+ cdnBypass = false;
80
70
 
81
- this.__handleSegmentUpdateCall();
71
+ if (backoff.timeoutID || !isHandlingEvent) __handleSegmentUpdateCall();
72
+ backoff.reset();
73
+ },
74
+ stop() {
75
+ isHandlingEvent = false;
76
+ backoff.reset();
77
+ }
78
+ };
82
79
  }
83
80
 
81
+ const segments: Record<string, ReturnType<typeof SegmentUpdateWorker>> = {};
82
+
83
+ return {
84
+ /**
85
+ * Invoked by NotificationProcessor on SEGMENT_UPDATE event
86
+ *
87
+ * @param {number} changeNumber change number of the SEGMENT_UPDATE notification
88
+ * @param {string} segmentName segment name of the SEGMENT_UPDATE notification
89
+ */
90
+ put({ changeNumber, segmentName }: ISegmentUpdateData) {
91
+ if (!segments[segmentName]) segments[segmentName] = SegmentUpdateWorker(segmentName);
92
+ segments[segmentName].put(changeNumber);
93
+ },
94
+
95
+ stop() {
96
+ Object.keys(segments).forEach(segmentName => segments[segmentName].stop());
97
+ }
98
+ };
84
99
  }
@@ -1,58 +1,63 @@
1
+ import { ILogger } from '../../../logger/types';
1
2
  import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants';
2
3
  import { ISplitsEventEmitter } from '../../../readiness/types';
3
4
  import { ISplitsCacheSync } from '../../../storages/types';
4
5
  import { Backoff } from '../../../utils/Backoff';
5
6
  import { ISegmentsSyncTask, ISplitsSyncTask } from '../../polling/types';
6
7
  import { ISplitKillData, ISplitUpdateData } from '../SSEHandler/types';
8
+ import { FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT, FETCH_BACKOFF_MAX_RETRIES } from './constants';
7
9
  import { IUpdateWorker } from './types';
8
10
 
9
11
  /**
10
- * SplitsUpdateWorker class
12
+ * SplitsUpdateWorker factory
11
13
  */
12
- export class SplitsUpdateWorker implements IUpdateWorker {
14
+ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker & { killSplit(event: ISplitKillData): void } {
13
15
 
14
- private readonly splitsCache: ISplitsCacheSync;
15
- private readonly splitsSyncTask: ISplitsSyncTask;
16
- private readonly splitsEventEmitter: ISplitsEventEmitter;
17
- private readonly segmentsSyncTask?: ISegmentsSyncTask;
18
- private maxChangeNumber: number;
19
- private handleNewEvent: boolean;
20
- readonly backoff: Backoff;
16
+ let maxChangeNumber = 0;
17
+ let handleNewEvent = false;
18
+ let isHandlingEvent: boolean;
19
+ let cdnBypass: boolean;
20
+ const backoff = new Backoff(__handleSplitUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT);
21
21
 
22
- /**
23
- * @param {Object} splitsCache splits data cache
24
- * @param {Object} splitsSyncTask task for syncing splits data
25
- * @param {Object} splitsEventEmitter emitter for splits data events
26
- */
27
- constructor(splitsCache: ISplitsCacheSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, segmentsSyncTask?: ISegmentsSyncTask) {
28
- this.splitsCache = splitsCache;
29
- this.splitsSyncTask = splitsSyncTask;
30
- this.splitsEventEmitter = splitsEventEmitter;
31
- this.segmentsSyncTask = segmentsSyncTask;
32
- this.maxChangeNumber = 0;
33
- this.handleNewEvent = false;
34
- this.put = this.put.bind(this);
35
- this.killSplit = this.killSplit.bind(this);
36
- this.__handleSplitUpdateCall = this.__handleSplitUpdateCall.bind(this);
37
- this.backoff = new Backoff(this.__handleSplitUpdateCall);
38
- }
39
-
40
- // Private method
41
- // Preconditions: this.splitsSyncTask.isSynchronizingSplits === false
42
- __handleSplitUpdateCall() {
43
- if (this.maxChangeNumber > this.splitsCache.getChangeNumber()) {
44
- this.handleNewEvent = false;
22
+ function __handleSplitUpdateCall() {
23
+ isHandlingEvent = true;
24
+ if (maxChangeNumber > splitsCache.getChangeNumber()) {
25
+ handleNewEvent = false;
45
26
 
46
27
  // fetch splits revalidating data if cached
47
- this.splitsSyncTask.execute(true).then(() => {
48
- if (this.handleNewEvent) {
49
- this.__handleSplitUpdateCall();
28
+ splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined).then(() => {
29
+ if (!isHandlingEvent) return; // halt if `stop` has been called
30
+ if (handleNewEvent) {
31
+ __handleSplitUpdateCall();
50
32
  } else {
51
33
  // fetch new registered segments for server-side API. Not retrying on error
52
- if (this.segmentsSyncTask) this.segmentsSyncTask.execute(undefined, false, true);
53
- this.backoff.scheduleCall();
34
+ if (segmentsSyncTask) segmentsSyncTask.execute(true);
35
+
36
+ const attempts = backoff.attempts + 1;
37
+
38
+ if (maxChangeNumber <= splitsCache.getChangeNumber()) {
39
+ log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`);
40
+ isHandlingEvent = false;
41
+ return;
42
+ }
43
+
44
+ if (attempts < FETCH_BACKOFF_MAX_RETRIES) {
45
+ backoff.scheduleCall();
46
+ return;
47
+ }
48
+
49
+ if (cdnBypass) {
50
+ log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`);
51
+ isHandlingEvent = false;
52
+ } else {
53
+ backoff.reset();
54
+ cdnBypass = true;
55
+ __handleSplitUpdateCall();
56
+ }
54
57
  }
55
58
  });
59
+ } else {
60
+ isHandlingEvent = false;
56
61
  }
57
62
  }
58
63
 
@@ -61,34 +66,41 @@ export class SplitsUpdateWorker implements IUpdateWorker {
61
66
  *
62
67
  * @param {number} changeNumber change number of the SPLIT_UPDATE notification
63
68
  */
64
- put({ changeNumber }: Pick<ISplitUpdateData, 'changeNumber'>) {
65
- const currentChangeNumber = this.splitsCache.getChangeNumber();
66
-
67
- if (changeNumber <= currentChangeNumber || changeNumber <= this.maxChangeNumber) return;
69
+ function put({ changeNumber }: Pick<ISplitUpdateData, 'changeNumber'>) {
70
+ const currentChangeNumber = splitsCache.getChangeNumber();
68
71
 
69
- this.maxChangeNumber = changeNumber;
70
- this.handleNewEvent = true;
71
- this.backoff.reset();
72
+ if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return;
72
73
 
73
- if (this.splitsSyncTask.isExecuting()) return;
74
+ maxChangeNumber = changeNumber;
75
+ handleNewEvent = true;
76
+ cdnBypass = false;
74
77
 
75
- this.__handleSplitUpdateCall();
78
+ if (backoff.timeoutID || !isHandlingEvent) __handleSplitUpdateCall();
79
+ backoff.reset();
76
80
  }
77
81
 
78
- /**
79
- * Invoked by NotificationProcessor on SPLIT_KILL event
80
- *
81
- * @param {number} changeNumber change number of the SPLIT_UPDATE notification
82
- * @param {string} splitName name of split to kill
83
- * @param {string} defaultTreatment default treatment value
84
- */
85
- killSplit({ changeNumber, splitName, defaultTreatment }: ISplitKillData) {
86
- if (this.splitsCache.killLocally(splitName, defaultTreatment, changeNumber)) {
87
- // trigger an SDK_UPDATE if Split was killed locally
88
- this.splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, true);
89
- }
90
- // queues the SplitChanges fetch (only if changeNumber is newer)
91
- this.put({ changeNumber });
92
- }
82
+ return {
83
+ put,
93
84
 
85
+ /**
86
+ * Invoked by NotificationProcessor on SPLIT_KILL event
87
+ *
88
+ * @param {number} changeNumber change number of the SPLIT_UPDATE notification
89
+ * @param {string} splitName name of split to kill
90
+ * @param {string} defaultTreatment default treatment value
91
+ */
92
+ killSplit({ changeNumber, splitName, defaultTreatment }: ISplitKillData) {
93
+ if (splitsCache.killLocally(splitName, defaultTreatment, changeNumber)) {
94
+ // trigger an SDK_UPDATE if Split was killed locally
95
+ splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, true);
96
+ }
97
+ // queues the SplitChanges fetch (only if changeNumber is newer)
98
+ put({ changeNumber });
99
+ },
100
+
101
+ stop() {
102
+ isHandlingEvent = false;
103
+ backoff.reset();
104
+ }
105
+ };
94
106
  }
@@ -0,0 +1,3 @@
1
+ export const FETCH_BACKOFF_BASE = 10000; // backoff base starting at 10 seconds
2
+ export const FETCH_BACKOFF_MAX_WAIT = 60000; // don't wait for more than 1 minute
3
+ export const FETCH_BACKOFF_MAX_RETRIES = 10; // max retries
@@ -1,6 +1,4 @@
1
- import { Backoff } from '../../../utils/Backoff';
2
-
3
1
  export interface IUpdateWorker {
4
- readonly backoff: Backoff,
5
- put(...args: any[]): void
2
+ stop(): void // clear scheduled tasks (backoff)
3
+ put(...args: any[]): void // handle new update event
6
4
  }