@splitsoftware/splitio-commons 1.4.1-rc.2 → 1.4.2-rc.1

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 (34) hide show
  1. package/CHANGES.txt +5 -0
  2. package/cjs/consent/sdkUserConsent.js +3 -3
  3. package/cjs/listeners/browser.js +7 -6
  4. package/cjs/sync/submitters/submitterManager.js +28 -4
  5. package/cjs/sync/syncManagerOnline.js +39 -28
  6. package/cjs/sync/syncTask.js +15 -10
  7. package/cjs/utils/settingsValidation/index.js +6 -1
  8. package/esm/consent/sdkUserConsent.js +3 -3
  9. package/esm/listeners/browser.js +7 -6
  10. package/esm/sync/submitters/submitterManager.js +28 -4
  11. package/esm/sync/syncManagerOnline.js +39 -28
  12. package/esm/sync/syncTask.js +15 -10
  13. package/esm/utils/settingsValidation/index.js +6 -1
  14. package/package.json +1 -1
  15. package/src/consent/sdkUserConsent.ts +4 -3
  16. package/src/listeners/browser.ts +8 -6
  17. package/src/sdkClient/clientAttributesDecoration.ts +9 -9
  18. package/src/sync/submitters/submitterManager.ts +30 -4
  19. package/src/sync/submitters/types.ts +7 -0
  20. package/src/sync/syncManagerOnline.ts +34 -22
  21. package/src/sync/syncTask.ts +17 -11
  22. package/src/sync/types.ts +2 -1
  23. package/src/types.ts +7 -1
  24. package/src/utils/settingsValidation/index.ts +7 -1
  25. package/types/integrations/ga/autoRequire.d.ts +4 -0
  26. package/types/sync/submitters/submitterManager.d.ts +2 -1
  27. package/types/sync/submitters/types.d.ts +6 -0
  28. package/types/sync/syncTask.d.ts +2 -2
  29. package/types/sync/types.d.ts +2 -1
  30. package/types/types.d.ts +6 -0
  31. package/types/utils/settingsValidation/index.d.ts +1 -0
  32. package/cjs/sync/syncTaskComposite.js +0 -26
  33. package/esm/sync/syncTaskComposite.js +0 -22
  34. package/src/sync/syncTaskComposite.ts +0 -26
package/CHANGES.txt CHANGED
@@ -1,3 +1,8 @@
1
+ 1.4.2 (June XX, 2022)
2
+ - Added `sync.enabled` property to SDK configuration to allow synchronize splits and segments only once.
3
+ - Updated telemetry to submit data even if user consent is not granted.
4
+ - Updated submitters logic, to avoid duplicating the post of impressions to Split cloud when the SDK is destroyed while performing its periodic post of impressions.
5
+
1
6
  1.4.1 (June 13, 2022)
2
7
  - Bugfixing - Updated submitters logic, to avoid dropping impressions and events that are being tracked while POST request is pending.
3
8
 
@@ -31,10 +31,10 @@ function createUserConsentAPI(params) {
31
31
  log.info(constants_1.USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); // @ts-ignore, modify readonly prop
32
32
  settings.userConsent = newConsentStatus;
33
33
  if (consent) { // resumes submitters if transitioning to GRANTED
34
- (_a = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitter) === null || _a === void 0 ? void 0 : _a.start();
34
+ (_a = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitterManager) === null || _a === void 0 ? void 0 : _a.start();
35
35
  }
36
- else { // pauses submitters and drops tracked data if transitioning to DECLINED
37
- (_b = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitter) === null || _b === void 0 ? void 0 : _b.stop();
36
+ else { // pauses submitters (except telemetry), and drops tracked data if transitioning to DECLINED
37
+ (_b = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitterManager) === null || _b === void 0 ? void 0 : _b.stop(true);
38
38
  // @ts-ignore, clear method is present in storage for standalone and partial consumer mode
39
39
  if (events.clear)
40
40
  events.clear(); // @ts-ignore
@@ -53,7 +53,7 @@ var BrowserSignalListener = /** @class */ (function () {
53
53
  BrowserSignalListener.prototype.flushData = function () {
54
54
  if (!this.syncManager)
55
55
  return; // In consumer mode there is not sync manager and data to flush
56
- // Flush data if there is user consent
56
+ // Flush impressions & events data if there is user consent
57
57
  if ((0, consent_1.isConsentGranted)(this.settings)) {
58
58
  var eventsUrl = this.settings.urls.events;
59
59
  var extraMetadata = {
@@ -64,11 +64,12 @@ var BrowserSignalListener = /** @class */ (function () {
64
64
  this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
65
65
  if (this.storage.impressionCounts)
66
66
  this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, impressionCountsSubmitter_1.fromImpressionCountsCollector);
67
- if (this.storage.telemetry) {
68
- var telemetryUrl = this.settings.urls.telemetry;
69
- var telemetryCacheAdapter = (0, telemetrySubmitter_1.telemetryCacheStatsAdapter)(this.storage.telemetry, this.storage.splits, this.storage.segments);
70
- this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage);
71
- }
67
+ }
68
+ // Flush telemetry data
69
+ if (this.storage.telemetry) {
70
+ var telemetryUrl = this.settings.urls.telemetry;
71
+ var telemetryCacheAdapter = (0, telemetrySubmitter_1.telemetryCacheStatsAdapter)(this.storage.telemetry, this.storage.splits, this.storage.segments);
72
+ this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage);
72
73
  }
73
74
  // Close streaming connection
74
75
  if (this.syncManager.pushManager)
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.submitterManagerFactory = void 0;
4
- var syncTaskComposite_1 = require("../syncTaskComposite");
5
4
  var eventsSubmitter_1 = require("./eventsSubmitter");
6
5
  var impressionsSubmitter_1 = require("./impressionsSubmitter");
7
6
  var impressionCountsSubmitter_1 = require("./impressionCountsSubmitter");
@@ -15,8 +14,33 @@ function submitterManagerFactory(params) {
15
14
  if (impressionCountsSubmitter)
16
15
  submitters.push(impressionCountsSubmitter);
17
16
  var telemetrySubmitter = (0, telemetrySubmitter_1.telemetrySubmitterFactory)(params);
18
- if (telemetrySubmitter)
19
- submitters.push(telemetrySubmitter);
20
- return (0, syncTaskComposite_1.syncTaskComposite)(submitters);
17
+ return {
18
+ // `onlyTelemetry` true if SDK is created with userConsent not GRANTED
19
+ start: function (onlyTelemetry) {
20
+ if (!onlyTelemetry)
21
+ submitters.forEach(function (submitter) { return submitter.start(); });
22
+ if (telemetrySubmitter)
23
+ telemetrySubmitter.start();
24
+ },
25
+ // `allExceptTelemetry` true if userConsent is changed to DECLINED
26
+ stop: function (allExceptTelemetry) {
27
+ submitters.forEach(function (submitter) { return submitter.stop(); });
28
+ if (!allExceptTelemetry && telemetrySubmitter)
29
+ telemetrySubmitter.stop();
30
+ },
31
+ isRunning: function () {
32
+ return submitters.some(function (submitter) { return submitter.isRunning(); });
33
+ },
34
+ // Flush data. Called with `onlyTelemetry` true if SDK is destroyed with userConsent not GRANTED
35
+ execute: function (onlyTelemetry) {
36
+ var promises = onlyTelemetry ? [] : submitters.map(function (submitter) { return submitter.execute(); });
37
+ if (telemetrySubmitter)
38
+ promises.push(telemetrySubmitter.execute());
39
+ return Promise.all(promises);
40
+ },
41
+ isExecuting: function () {
42
+ return submitters.some(function (submitter) { return submitter.isExecuting(); });
43
+ }
44
+ };
21
45
  }
22
46
  exports.submitterManagerFactory = submitterManagerFactory;
@@ -19,16 +19,16 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
19
19
  * SyncManager factory for modular SDK
20
20
  */
21
21
  return function (params) {
22
- var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, telemetryTracker = params.telemetryTracker;
22
+ var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, syncEnabled = _a.sync.enabled, telemetryTracker = params.telemetryTracker;
23
23
  /** Polling Manager */
24
24
  var pollingManager = pollingManagerFactory && pollingManagerFactory(params);
25
25
  /** Push Manager */
26
- var pushManager = streamingEnabled && pollingManager && pushManagerFactory ?
26
+ var pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ?
27
27
  pushManagerFactory(params, pollingManager) :
28
28
  undefined;
29
29
  /** Submitter Manager */
30
30
  // It is not inyected as push and polling managers, because at the moment it is required
31
- var submitter = (0, submitterManager_1.submitterManagerFactory)(params);
31
+ var submitterManager = (0, submitterManager_1.submitterManagerFactory)(params);
32
32
  /** Sync Manager logic */
33
33
  function startPolling() {
34
34
  if (pollingManager.isRunning()) {
@@ -61,7 +61,7 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
61
61
  // E.g.: user consent, app state changes (Page hide, Foreground/Background, Online/Offline).
62
62
  pollingManager: pollingManager,
63
63
  pushManager: pushManager,
64
- submitter: submitter,
64
+ submitterManager: submitterManager,
65
65
  /**
66
66
  * Method used to start the syncManager for the first time, or resume it after being stopped.
67
67
  */
@@ -69,21 +69,29 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
69
69
  running = true;
70
70
  // start syncing splits and segments
71
71
  if (pollingManager) {
72
- if (pushManager) {
73
- // Doesn't call `syncAll` when the syncManager is resuming
72
+ // If synchronization is disabled pushManager and pollingManager should not start
73
+ if (syncEnabled) {
74
+ if (pushManager) {
75
+ // Doesn't call `syncAll` when the syncManager is resuming
76
+ if (startFirstTime) {
77
+ pollingManager.syncAll();
78
+ startFirstTime = false;
79
+ }
80
+ pushManager.start();
81
+ }
82
+ else {
83
+ pollingManager.start();
84
+ }
85
+ }
86
+ else {
74
87
  if (startFirstTime) {
75
88
  pollingManager.syncAll();
76
89
  startFirstTime = false;
77
90
  }
78
- pushManager.start();
79
- }
80
- else {
81
- pollingManager.start();
82
91
  }
83
92
  }
84
93
  // start periodic data recording (events, impressions, telemetry).
85
- if ((0, consent_1.isConsentGranted)(settings))
86
- submitter.start();
94
+ submitterManager.start(!(0, consent_1.isConsentGranted)(settings));
87
95
  },
88
96
  /**
89
97
  * Method used to stop/pause the syncManager.
@@ -96,16 +104,13 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
96
104
  if (pollingManager && pollingManager.isRunning())
97
105
  pollingManager.stop();
98
106
  // stop periodic data recording (events, impressions, telemetry).
99
- submitter.stop();
107
+ submitterManager.stop();
100
108
  },
101
109
  isRunning: function () {
102
110
  return running;
103
111
  },
104
112
  flush: function () {
105
- if ((0, consent_1.isConsentGranted)(settings))
106
- return submitter.execute();
107
- else
108
- return Promise.resolve();
113
+ return submitterManager.execute(!(0, consent_1.isConsentGranted)(settings));
109
114
  },
110
115
  // [Only used for client-side]
111
116
  // If polling and push managers are defined (standalone mode), they implement the interfaces for client-side
@@ -116,22 +121,28 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
116
121
  return {
117
122
  isRunning: mySegmentsSyncTask.isRunning,
118
123
  start: function () {
119
- if (pushManager) {
120
- if (pollingManager.isRunning()) {
121
- // if doing polling, we must start the periodic fetch of data
122
- if (storage.splits.usesSegments())
123
- mySegmentsSyncTask.start();
124
+ if (syncEnabled) {
125
+ if (pushManager) {
126
+ if (pollingManager.isRunning()) {
127
+ // if doing polling, we must start the periodic fetch of data
128
+ if (storage.splits.usesSegments())
129
+ mySegmentsSyncTask.start();
130
+ }
131
+ else {
132
+ // if not polling, we must execute the sync task for the initial fetch
133
+ // of segments since `syncAll` was already executed when starting the main client
134
+ mySegmentsSyncTask.execute();
135
+ }
136
+ pushManager.add(matchingKey, mySegmentsSyncTask);
124
137
  }
125
138
  else {
126
- // if not polling, we must execute the sync task for the initial fetch
127
- // of segments since `syncAll` was already executed when starting the main client
128
- mySegmentsSyncTask.execute();
139
+ if (storage.splits.usesSegments())
140
+ mySegmentsSyncTask.start();
129
141
  }
130
- pushManager.add(matchingKey, mySegmentsSyncTask);
131
142
  }
132
143
  else {
133
- if (storage.splits.usesSegments())
134
- mySegmentsSyncTask.start();
144
+ if (!readinessManager.isReady())
145
+ mySegmentsSyncTask.execute();
135
146
  }
136
147
  },
137
148
  stop: function () {
@@ -4,8 +4,8 @@ exports.syncTaskFactory = void 0;
4
4
  var constants_1 = require("../logger/constants");
5
5
  /**
6
6
  * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods).
7
- * The task can be executed once calling the "execute" method.
8
- * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization.
7
+ * The task can be also executed by calling the "execute" method. Multiple execute calls are chained to run secuentially and avoid race conditions.
8
+ * For example, submitters executed on SDK destroy or full queue, while periodic execution is pending.
9
9
  *
10
10
  * @param log Logger instance.
11
11
  * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections.
@@ -15,8 +15,8 @@ var constants_1 = require("../logger/constants");
15
15
  */
16
16
  function syncTaskFactory(log, task, period, taskName) {
17
17
  if (taskName === void 0) { taskName = 'task'; }
18
- // Flag that indicates if the task is being executed
19
- var executing = false;
18
+ // Task promise while it is pending. Undefined once the promise is resolved
19
+ var pendingTask;
20
20
  // flag that indicates if the task periodic execution has been started/stopped.
21
21
  var running = false;
22
22
  // Auxiliar counter used to avoid race condition when calling `start` & `stop` intermittently
@@ -30,13 +30,19 @@ function syncTaskFactory(log, task, period, taskName) {
30
30
  for (var _i = 0; _i < arguments.length; _i++) {
31
31
  args[_i] = arguments[_i];
32
32
  }
33
- executing = true;
33
+ // If task is executing, chain the new execution
34
+ if (pendingTask) {
35
+ return pendingTask.then(function () {
36
+ return execute.apply(void 0, args);
37
+ });
38
+ }
39
+ // Execute task
34
40
  log.debug(constants_1.SYNC_TASK_EXECUTE, [taskName]);
35
- return task.apply(void 0, args).then(function (result) {
36
- executing = false;
41
+ pendingTask = task.apply(void 0, args).then(function (result) {
42
+ pendingTask = undefined;
37
43
  return result;
38
44
  });
39
- // No need to handle promise rejection because it is a pre-condition that provided task never rejects.
45
+ return pendingTask;
40
46
  }
41
47
  function periodicExecute(currentRunningId) {
42
48
  return execute.apply(void 0, runningArgs).then(function (result) {
@@ -48,10 +54,9 @@ function syncTaskFactory(log, task, period, taskName) {
48
54
  });
49
55
  }
50
56
  return {
51
- // @TODO check if we need to queued `execute` calls, to avoid possible race conditions on submitters and updaters with streaming.
52
57
  execute: execute,
53
58
  isExecuting: function () {
54
- return executing;
59
+ return pendingTask !== undefined;
55
60
  },
56
61
  start: function () {
57
62
  var args = [];
@@ -73,7 +73,8 @@ exports.base = {
73
73
  splitFilters: undefined,
74
74
  // impressions collection mode
75
75
  impressionsMode: constants_1.OPTIMIZED,
76
- localhostMode: undefined
76
+ localhostMode: undefined,
77
+ enabled: true
77
78
  },
78
79
  // Logger
79
80
  log: undefined
@@ -167,6 +168,10 @@ function settingsValidation(config, validationParams) {
167
168
  // We are not checking if bases are positive numbers. Thus, we might be reauthenticating immediately (`setTimeout` with NaN or negative number)
168
169
  scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase);
169
170
  }
171
+ // validate sync enabled
172
+ if (withDefaults.sync.enabled !== false) { // @ts-ignore, modify readonly prop
173
+ withDefaults.sync.enabled = true;
174
+ }
170
175
  // validate the `splitFilters` settings and parse splits query
171
176
  var splitFiltersValidation = (0, splitFilters_1.validateSplitFilters)(log, withDefaults.sync.splitFilters, withDefaults.mode);
172
177
  withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;
@@ -28,10 +28,10 @@ export function createUserConsentAPI(params) {
28
28
  log.info(USER_CONSENT_UPDATED, [settings.userConsent, newConsentStatus]); // @ts-ignore, modify readonly prop
29
29
  settings.userConsent = newConsentStatus;
30
30
  if (consent) { // resumes submitters if transitioning to GRANTED
31
- (_a = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitter) === null || _a === void 0 ? void 0 : _a.start();
31
+ (_a = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitterManager) === null || _a === void 0 ? void 0 : _a.start();
32
32
  }
33
- else { // pauses submitters and drops tracked data if transitioning to DECLINED
34
- (_b = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitter) === null || _b === void 0 ? void 0 : _b.stop();
33
+ else { // pauses submitters (except telemetry), and drops tracked data if transitioning to DECLINED
34
+ (_b = syncManager === null || syncManager === void 0 ? void 0 : syncManager.submitterManager) === null || _b === void 0 ? void 0 : _b.stop(true);
35
35
  // @ts-ignore, clear method is present in storage for standalone and partial consumer mode
36
36
  if (events.clear)
37
37
  events.clear(); // @ts-ignore
@@ -50,7 +50,7 @@ var BrowserSignalListener = /** @class */ (function () {
50
50
  BrowserSignalListener.prototype.flushData = function () {
51
51
  if (!this.syncManager)
52
52
  return; // In consumer mode there is not sync manager and data to flush
53
- // Flush data if there is user consent
53
+ // Flush impressions & events data if there is user consent
54
54
  if (isConsentGranted(this.settings)) {
55
55
  var eventsUrl = this.settings.urls.events;
56
56
  var extraMetadata = {
@@ -61,11 +61,12 @@ var BrowserSignalListener = /** @class */ (function () {
61
61
  this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
62
62
  if (this.storage.impressionCounts)
63
63
  this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
64
- if (this.storage.telemetry) {
65
- var telemetryUrl = this.settings.urls.telemetry;
66
- var telemetryCacheAdapter = telemetryCacheStatsAdapter(this.storage.telemetry, this.storage.splits, this.storage.segments);
67
- this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage);
68
- }
64
+ }
65
+ // Flush telemetry data
66
+ if (this.storage.telemetry) {
67
+ var telemetryUrl = this.settings.urls.telemetry;
68
+ var telemetryCacheAdapter = telemetryCacheStatsAdapter(this.storage.telemetry, this.storage.splits, this.storage.segments);
69
+ this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage);
69
70
  }
70
71
  // Close streaming connection
71
72
  if (this.syncManager.pushManager)
@@ -1,4 +1,3 @@
1
- import { syncTaskComposite } from '../syncTaskComposite';
2
1
  import { eventsSubmitterFactory } from './eventsSubmitter';
3
2
  import { impressionsSubmitterFactory } from './impressionsSubmitter';
4
3
  import { impressionCountsSubmitterFactory } from './impressionCountsSubmitter';
@@ -12,7 +11,32 @@ export function submitterManagerFactory(params) {
12
11
  if (impressionCountsSubmitter)
13
12
  submitters.push(impressionCountsSubmitter);
14
13
  var telemetrySubmitter = telemetrySubmitterFactory(params);
15
- if (telemetrySubmitter)
16
- submitters.push(telemetrySubmitter);
17
- return syncTaskComposite(submitters);
14
+ return {
15
+ // `onlyTelemetry` true if SDK is created with userConsent not GRANTED
16
+ start: function (onlyTelemetry) {
17
+ if (!onlyTelemetry)
18
+ submitters.forEach(function (submitter) { return submitter.start(); });
19
+ if (telemetrySubmitter)
20
+ telemetrySubmitter.start();
21
+ },
22
+ // `allExceptTelemetry` true if userConsent is changed to DECLINED
23
+ stop: function (allExceptTelemetry) {
24
+ submitters.forEach(function (submitter) { return submitter.stop(); });
25
+ if (!allExceptTelemetry && telemetrySubmitter)
26
+ telemetrySubmitter.stop();
27
+ },
28
+ isRunning: function () {
29
+ return submitters.some(function (submitter) { return submitter.isRunning(); });
30
+ },
31
+ // Flush data. Called with `onlyTelemetry` true if SDK is destroyed with userConsent not GRANTED
32
+ execute: function (onlyTelemetry) {
33
+ var promises = onlyTelemetry ? [] : submitters.map(function (submitter) { return submitter.execute(); });
34
+ if (telemetrySubmitter)
35
+ promises.push(telemetrySubmitter.execute());
36
+ return Promise.all(promises);
37
+ },
38
+ isExecuting: function () {
39
+ return submitters.some(function (submitter) { return submitter.isExecuting(); });
40
+ }
41
+ };
18
42
  }
@@ -16,16 +16,16 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
16
16
  * SyncManager factory for modular SDK
17
17
  */
18
18
  return function (params) {
19
- var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, telemetryTracker = params.telemetryTracker;
19
+ var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, syncEnabled = _a.sync.enabled, telemetryTracker = params.telemetryTracker;
20
20
  /** Polling Manager */
21
21
  var pollingManager = pollingManagerFactory && pollingManagerFactory(params);
22
22
  /** Push Manager */
23
- var pushManager = streamingEnabled && pollingManager && pushManagerFactory ?
23
+ var pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ?
24
24
  pushManagerFactory(params, pollingManager) :
25
25
  undefined;
26
26
  /** Submitter Manager */
27
27
  // It is not inyected as push and polling managers, because at the moment it is required
28
- var submitter = submitterManagerFactory(params);
28
+ var submitterManager = submitterManagerFactory(params);
29
29
  /** Sync Manager logic */
30
30
  function startPolling() {
31
31
  if (pollingManager.isRunning()) {
@@ -58,7 +58,7 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
58
58
  // E.g.: user consent, app state changes (Page hide, Foreground/Background, Online/Offline).
59
59
  pollingManager: pollingManager,
60
60
  pushManager: pushManager,
61
- submitter: submitter,
61
+ submitterManager: submitterManager,
62
62
  /**
63
63
  * Method used to start the syncManager for the first time, or resume it after being stopped.
64
64
  */
@@ -66,21 +66,29 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
66
66
  running = true;
67
67
  // start syncing splits and segments
68
68
  if (pollingManager) {
69
- if (pushManager) {
70
- // Doesn't call `syncAll` when the syncManager is resuming
69
+ // If synchronization is disabled pushManager and pollingManager should not start
70
+ if (syncEnabled) {
71
+ if (pushManager) {
72
+ // Doesn't call `syncAll` when the syncManager is resuming
73
+ if (startFirstTime) {
74
+ pollingManager.syncAll();
75
+ startFirstTime = false;
76
+ }
77
+ pushManager.start();
78
+ }
79
+ else {
80
+ pollingManager.start();
81
+ }
82
+ }
83
+ else {
71
84
  if (startFirstTime) {
72
85
  pollingManager.syncAll();
73
86
  startFirstTime = false;
74
87
  }
75
- pushManager.start();
76
- }
77
- else {
78
- pollingManager.start();
79
88
  }
80
89
  }
81
90
  // start periodic data recording (events, impressions, telemetry).
82
- if (isConsentGranted(settings))
83
- submitter.start();
91
+ submitterManager.start(!isConsentGranted(settings));
84
92
  },
85
93
  /**
86
94
  * Method used to stop/pause the syncManager.
@@ -93,16 +101,13 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
93
101
  if (pollingManager && pollingManager.isRunning())
94
102
  pollingManager.stop();
95
103
  // stop periodic data recording (events, impressions, telemetry).
96
- submitter.stop();
104
+ submitterManager.stop();
97
105
  },
98
106
  isRunning: function () {
99
107
  return running;
100
108
  },
101
109
  flush: function () {
102
- if (isConsentGranted(settings))
103
- return submitter.execute();
104
- else
105
- return Promise.resolve();
110
+ return submitterManager.execute(!isConsentGranted(settings));
106
111
  },
107
112
  // [Only used for client-side]
108
113
  // If polling and push managers are defined (standalone mode), they implement the interfaces for client-side
@@ -113,22 +118,28 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
113
118
  return {
114
119
  isRunning: mySegmentsSyncTask.isRunning,
115
120
  start: function () {
116
- if (pushManager) {
117
- if (pollingManager.isRunning()) {
118
- // if doing polling, we must start the periodic fetch of data
119
- if (storage.splits.usesSegments())
120
- mySegmentsSyncTask.start();
121
+ if (syncEnabled) {
122
+ if (pushManager) {
123
+ if (pollingManager.isRunning()) {
124
+ // if doing polling, we must start the periodic fetch of data
125
+ if (storage.splits.usesSegments())
126
+ mySegmentsSyncTask.start();
127
+ }
128
+ else {
129
+ // if not polling, we must execute the sync task for the initial fetch
130
+ // of segments since `syncAll` was already executed when starting the main client
131
+ mySegmentsSyncTask.execute();
132
+ }
133
+ pushManager.add(matchingKey, mySegmentsSyncTask);
121
134
  }
122
135
  else {
123
- // if not polling, we must execute the sync task for the initial fetch
124
- // of segments since `syncAll` was already executed when starting the main client
125
- mySegmentsSyncTask.execute();
136
+ if (storage.splits.usesSegments())
137
+ mySegmentsSyncTask.start();
126
138
  }
127
- pushManager.add(matchingKey, mySegmentsSyncTask);
128
139
  }
129
140
  else {
130
- if (storage.splits.usesSegments())
131
- mySegmentsSyncTask.start();
141
+ if (!readinessManager.isReady())
142
+ mySegmentsSyncTask.execute();
132
143
  }
133
144
  },
134
145
  stop: function () {
@@ -1,8 +1,8 @@
1
1
  import { SYNC_TASK_EXECUTE, SYNC_TASK_START, SYNC_TASK_STOP } from '../logger/constants';
2
2
  /**
3
3
  * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods).
4
- * The task can be executed once calling the "execute" method.
5
- * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization.
4
+ * The task can be also executed by calling the "execute" method. Multiple execute calls are chained to run secuentially and avoid race conditions.
5
+ * For example, submitters executed on SDK destroy or full queue, while periodic execution is pending.
6
6
  *
7
7
  * @param log Logger instance.
8
8
  * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections.
@@ -12,8 +12,8 @@ import { SYNC_TASK_EXECUTE, SYNC_TASK_START, SYNC_TASK_STOP } from '../logger/co
12
12
  */
13
13
  export function syncTaskFactory(log, task, period, taskName) {
14
14
  if (taskName === void 0) { taskName = 'task'; }
15
- // Flag that indicates if the task is being executed
16
- var executing = false;
15
+ // Task promise while it is pending. Undefined once the promise is resolved
16
+ var pendingTask;
17
17
  // flag that indicates if the task periodic execution has been started/stopped.
18
18
  var running = false;
19
19
  // Auxiliar counter used to avoid race condition when calling `start` & `stop` intermittently
@@ -27,13 +27,19 @@ export function syncTaskFactory(log, task, period, taskName) {
27
27
  for (var _i = 0; _i < arguments.length; _i++) {
28
28
  args[_i] = arguments[_i];
29
29
  }
30
- executing = true;
30
+ // If task is executing, chain the new execution
31
+ if (pendingTask) {
32
+ return pendingTask.then(function () {
33
+ return execute.apply(void 0, args);
34
+ });
35
+ }
36
+ // Execute task
31
37
  log.debug(SYNC_TASK_EXECUTE, [taskName]);
32
- return task.apply(void 0, args).then(function (result) {
33
- executing = false;
38
+ pendingTask = task.apply(void 0, args).then(function (result) {
39
+ pendingTask = undefined;
34
40
  return result;
35
41
  });
36
- // No need to handle promise rejection because it is a pre-condition that provided task never rejects.
42
+ return pendingTask;
37
43
  }
38
44
  function periodicExecute(currentRunningId) {
39
45
  return execute.apply(void 0, runningArgs).then(function (result) {
@@ -45,10 +51,9 @@ export function syncTaskFactory(log, task, period, taskName) {
45
51
  });
46
52
  }
47
53
  return {
48
- // @TODO check if we need to queued `execute` calls, to avoid possible race conditions on submitters and updaters with streaming.
49
54
  execute: execute,
50
55
  isExecuting: function () {
51
- return executing;
56
+ return pendingTask !== undefined;
52
57
  },
53
58
  start: function () {
54
59
  var args = [];
@@ -70,7 +70,8 @@ export var base = {
70
70
  splitFilters: undefined,
71
71
  // impressions collection mode
72
72
  impressionsMode: OPTIMIZED,
73
- localhostMode: undefined
73
+ localhostMode: undefined,
74
+ enabled: true
74
75
  },
75
76
  // Logger
76
77
  log: undefined
@@ -164,6 +165,10 @@ export function settingsValidation(config, validationParams) {
164
165
  // We are not checking if bases are positive numbers. Thus, we might be reauthenticating immediately (`setTimeout` with NaN or negative number)
165
166
  scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase);
166
167
  }
168
+ // validate sync enabled
169
+ if (withDefaults.sync.enabled !== false) { // @ts-ignore, modify readonly prop
170
+ withDefaults.sync.enabled = true;
171
+ }
167
172
  // validate the `splitFilters` settings and parse splits query
168
173
  var splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode);
169
174
  withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "1.4.1-rc.2",
3
+ "version": "1.4.2-rc.1",
4
4
  "description": "Split Javascript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -34,9 +34,10 @@ export function createUserConsentAPI(params: ISdkFactoryContext) {
34
34
  settings.userConsent = newConsentStatus;
35
35
 
36
36
  if (consent) { // resumes submitters if transitioning to GRANTED
37
- syncManager?.submitter?.start();
38
- } else { // pauses submitters and drops tracked data if transitioning to DECLINED
39
- syncManager?.submitter?.stop();
37
+ syncManager?.submitterManager?.start();
38
+ } else { // pauses submitters (except telemetry), and drops tracked data if transitioning to DECLINED
39
+ syncManager?.submitterManager?.stop(true);
40
+
40
41
  // @ts-ignore, clear method is present in storage for standalone and partial consumer mode
41
42
  if (events.clear) events.clear(); // @ts-ignore
42
43
  if (impressions.clear) impressions.clear();
@@ -67,7 +67,7 @@ export class BrowserSignalListener implements ISignalListener {
67
67
  flushData() {
68
68
  if (!this.syncManager) return; // In consumer mode there is not sync manager and data to flush
69
69
 
70
- // Flush data if there is user consent
70
+ // Flush impressions & events data if there is user consent
71
71
  if (isConsentGranted(this.settings)) {
72
72
  const eventsUrl = this.settings.urls.events;
73
73
  const extraMetadata = {
@@ -78,11 +78,13 @@ export class BrowserSignalListener implements ISignalListener {
78
78
  this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
79
79
  this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
80
80
  if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
81
- if (this.storage.telemetry) {
82
- const telemetryUrl = this.settings.urls.telemetry;
83
- const telemetryCacheAdapter = telemetryCacheStatsAdapter(this.storage.telemetry, this.storage.splits, this.storage.segments);
84
- this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage);
85
- }
81
+ }
82
+
83
+ // Flush telemetry data
84
+ if (this.storage.telemetry) {
85
+ const telemetryUrl = this.settings.urls.telemetry;
86
+ const telemetryCacheAdapter = telemetryCacheStatsAdapter(this.storage.telemetry, this.storage.splits, this.storage.segments);
87
+ this._flushData(telemetryUrl + '/v1/metrics/usage/beacon', telemetryCacheAdapter, this.serviceApi.postMetricsUsage);
86
88
  }
87
89
 
88
90
  // Close streaming connection
@@ -5,7 +5,7 @@ import { ILogger } from '../logger/types';
5
5
  import { objectAssign } from '../utils/lang/objectAssign';
6
6
 
7
7
  /**
8
- * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call
8
+ * Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call
9
9
  */
10
10
  export function clientAttributesDecoration<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(log: ILogger, client: TClient) {
11
11
 
@@ -52,10 +52,10 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
52
52
  getTreatments: getTreatments,
53
53
  getTreatmentsWithConfig: getTreatmentsWithConfig,
54
54
  track: track,
55
-
55
+
56
56
  /**
57
57
  * Add an attribute to client's in memory attributes storage
58
- *
58
+ *
59
59
  * @param {string} attributeName Attrinute name
60
60
  * @param {string, number, boolean, list} attributeValue Attribute value
61
61
  * @returns {boolean} true if the attribute was stored and false otherways
@@ -70,7 +70,7 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
70
70
 
71
71
  /**
72
72
  * Returns the attribute with the given key
73
- *
73
+ *
74
74
  * @param {string} attributeName Attribute name
75
75
  * @returns {Object} Attribute with the given key
76
76
  */
@@ -81,7 +81,7 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
81
81
 
82
82
  /**
83
83
  * Add to client's in memory attributes storage the attributes in 'attributes'
84
- *
84
+ *
85
85
  * @param {Object} attributes Object with attributes to store
86
86
  * @returns true if attributes were stored an false otherways
87
87
  */
@@ -92,7 +92,7 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
92
92
 
93
93
  /**
94
94
  * Return all the attributes stored in client's in memory attributes storage
95
- *
95
+ *
96
96
  * @returns {Object} returns all the stored attributes
97
97
  */
98
98
  getAttributes(): Record<string, Object> {
@@ -101,8 +101,8 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
101
101
 
102
102
  /**
103
103
  * Removes from client's in memory attributes storage the attribute with the given key
104
- *
105
- * @param {string} attributeName
104
+ *
105
+ * @param {string} attributeName
106
106
  * @returns {boolean} true if attribute was removed and false otherways
107
107
  */
108
108
  removeAttribute(attributeName: string) {
@@ -116,7 +116,7 @@ export function clientAttributesDecoration<TClient extends SplitIO.IClient | Spl
116
116
  clearAttributes() {
117
117
  return attributeStorage.clear();
118
118
  }
119
-
119
+
120
120
  });
121
121
 
122
122
  }
@@ -1,11 +1,11 @@
1
- import { syncTaskComposite } from '../syncTaskComposite';
2
1
  import { eventsSubmitterFactory } from './eventsSubmitter';
3
2
  import { impressionsSubmitterFactory } from './impressionsSubmitter';
4
3
  import { impressionCountsSubmitterFactory } from './impressionCountsSubmitter';
5
4
  import { telemetrySubmitterFactory } from './telemetrySubmitter';
6
5
  import { ISdkFactoryContextSync } from '../../sdkFactory/types';
6
+ import { ISubmitterManager } from './types';
7
7
 
8
- export function submitterManagerFactory(params: ISdkFactoryContextSync) {
8
+ export function submitterManagerFactory(params: ISdkFactoryContextSync): ISubmitterManager {
9
9
 
10
10
  const submitters = [
11
11
  impressionsSubmitterFactory(params),
@@ -15,7 +15,33 @@ export function submitterManagerFactory(params: ISdkFactoryContextSync) {
15
15
  const impressionCountsSubmitter = impressionCountsSubmitterFactory(params);
16
16
  if (impressionCountsSubmitter) submitters.push(impressionCountsSubmitter);
17
17
  const telemetrySubmitter = telemetrySubmitterFactory(params);
18
- if (telemetrySubmitter) submitters.push(telemetrySubmitter);
19
18
 
20
- return syncTaskComposite(submitters);
19
+ return {
20
+ // `onlyTelemetry` true if SDK is created with userConsent not GRANTED
21
+ start(onlyTelemetry?: boolean) {
22
+ if (!onlyTelemetry) submitters.forEach(submitter => submitter.start());
23
+ if (telemetrySubmitter) telemetrySubmitter.start();
24
+ },
25
+
26
+ // `allExceptTelemetry` true if userConsent is changed to DECLINED
27
+ stop(allExceptTelemetry?: boolean) {
28
+ submitters.forEach(submitter => submitter.stop());
29
+ if (!allExceptTelemetry && telemetrySubmitter) telemetrySubmitter.stop();
30
+ },
31
+
32
+ isRunning() {
33
+ return submitters.some(submitter => submitter.isRunning());
34
+ },
35
+
36
+ // Flush data. Called with `onlyTelemetry` true if SDK is destroyed with userConsent not GRANTED
37
+ execute(onlyTelemetry?: boolean) {
38
+ const promises = onlyTelemetry ? [] : submitters.map(submitter => submitter.execute());
39
+ if (telemetrySubmitter) promises.push(telemetrySubmitter.execute());
40
+ return Promise.all(promises);
41
+ },
42
+
43
+ isExecuting() {
44
+ return submitters.some(submitter => submitter.isExecuting());
45
+ }
46
+ };
21
47
  }
@@ -1,5 +1,6 @@
1
1
  import { IMetadata } from '../../dtos/types';
2
2
  import { SplitIO } from '../../types';
3
+ import { ISyncTask } from '../types';
3
4
 
4
5
  export type ImpressionsPayload = {
5
6
  /** Split name */
@@ -191,3 +192,9 @@ export type TelemetryConfigStatsPayload = TelemetryConfigStats & {
191
192
  i?: Array<string>, // integrations
192
193
  uC: number, // userConsent
193
194
  }
195
+
196
+ export interface ISubmitterManager extends ISyncTask {
197
+ start(onlyTelemetry?: boolean): void,
198
+ stop(allExceptTelemetry?: boolean): void,
199
+ execute(onlyTelemetry?: boolean): Promise<any>
200
+ }
@@ -28,19 +28,19 @@ export function syncManagerOnlineFactory(
28
28
  */
29
29
  return function (params: ISdkFactoryContextSync): ISyncManagerCS {
30
30
 
31
- const { settings, settings: { log, streamingEnabled }, telemetryTracker } = params;
31
+ const { settings, settings: { log, streamingEnabled, sync: { enabled: syncEnabled } }, telemetryTracker } = params;
32
32
 
33
33
  /** Polling Manager */
34
34
  const pollingManager = pollingManagerFactory && pollingManagerFactory(params);
35
35
 
36
36
  /** Push Manager */
37
- const pushManager = streamingEnabled && pollingManager && pushManagerFactory ?
37
+ const pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ?
38
38
  pushManagerFactory(params, pollingManager) :
39
39
  undefined;
40
40
 
41
41
  /** Submitter Manager */
42
42
  // It is not inyected as push and polling managers, because at the moment it is required
43
- const submitter = submitterManagerFactory(params);
43
+ const submitterManager = submitterManagerFactory(params);
44
44
 
45
45
  /** Sync Manager logic */
46
46
 
@@ -79,7 +79,7 @@ export function syncManagerOnlineFactory(
79
79
  // E.g.: user consent, app state changes (Page hide, Foreground/Background, Online/Offline).
80
80
  pollingManager,
81
81
  pushManager,
82
- submitter,
82
+ submitterManager,
83
83
 
84
84
  /**
85
85
  * Method used to start the syncManager for the first time, or resume it after being stopped.
@@ -89,20 +89,29 @@ export function syncManagerOnlineFactory(
89
89
 
90
90
  // start syncing splits and segments
91
91
  if (pollingManager) {
92
- if (pushManager) {
93
- // Doesn't call `syncAll` when the syncManager is resuming
92
+
93
+ // If synchronization is disabled pushManager and pollingManager should not start
94
+ if (syncEnabled) {
95
+ if (pushManager) {
96
+ // Doesn't call `syncAll` when the syncManager is resuming
97
+ if (startFirstTime) {
98
+ pollingManager.syncAll();
99
+ startFirstTime = false;
100
+ }
101
+ pushManager.start();
102
+ } else {
103
+ pollingManager.start();
104
+ }
105
+ } else {
94
106
  if (startFirstTime) {
95
107
  pollingManager.syncAll();
96
108
  startFirstTime = false;
97
109
  }
98
- pushManager.start();
99
- } else {
100
- pollingManager.start();
101
110
  }
102
111
  }
103
112
 
104
113
  // start periodic data recording (events, impressions, telemetry).
105
- if (isConsentGranted(settings)) submitter.start();
114
+ submitterManager.start(!isConsentGranted(settings));
106
115
  },
107
116
 
108
117
  /**
@@ -116,7 +125,7 @@ export function syncManagerOnlineFactory(
116
125
  if (pollingManager && pollingManager.isRunning()) pollingManager.stop();
117
126
 
118
127
  // stop periodic data recording (events, impressions, telemetry).
119
- submitter.stop();
128
+ submitterManager.stop();
120
129
  },
121
130
 
122
131
  isRunning() {
@@ -124,8 +133,7 @@ export function syncManagerOnlineFactory(
124
133
  },
125
134
 
126
135
  flush() {
127
- if (isConsentGranted(settings)) return submitter.execute();
128
- else return Promise.resolve();
136
+ return submitterManager.execute(!isConsentGranted(settings));
129
137
  },
130
138
 
131
139
  // [Only used for client-side]
@@ -138,18 +146,22 @@ export function syncManagerOnlineFactory(
138
146
  return {
139
147
  isRunning: mySegmentsSyncTask.isRunning,
140
148
  start() {
141
- if (pushManager) {
142
- if (pollingManager!.isRunning()) {
143
- // if doing polling, we must start the periodic fetch of data
144
- if (storage.splits.usesSegments()) mySegmentsSyncTask.start();
149
+ if (syncEnabled) {
150
+ if (pushManager) {
151
+ if (pollingManager!.isRunning()) {
152
+ // if doing polling, we must start the periodic fetch of data
153
+ if (storage.splits.usesSegments()) mySegmentsSyncTask.start();
154
+ } else {
155
+ // if not polling, we must execute the sync task for the initial fetch
156
+ // of segments since `syncAll` was already executed when starting the main client
157
+ mySegmentsSyncTask.execute();
158
+ }
159
+ pushManager.add(matchingKey, mySegmentsSyncTask);
145
160
  } else {
146
- // if not polling, we must execute the sync task for the initial fetch
147
- // of segments since `syncAll` was already executed when starting the main client
148
- mySegmentsSyncTask.execute();
161
+ if (storage.splits.usesSegments()) mySegmentsSyncTask.start();
149
162
  }
150
- pushManager.add(matchingKey, mySegmentsSyncTask);
151
163
  } else {
152
- if (storage.splits.usesSegments()) mySegmentsSyncTask.start();
164
+ if (!readinessManager.isReady()) mySegmentsSyncTask.execute();
153
165
  }
154
166
  },
155
167
  stop() {
@@ -4,8 +4,8 @@ import { ISyncTask } from './types';
4
4
 
5
5
  /**
6
6
  * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods).
7
- * The task can be executed once calling the "execute" method.
8
- * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization.
7
+ * The task can be also executed by calling the "execute" method. Multiple execute calls are chained to run secuentially and avoid race conditions.
8
+ * For example, submitters executed on SDK destroy or full queue, while periodic execution is pending.
9
9
  *
10
10
  * @param log Logger instance.
11
11
  * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections.
@@ -15,8 +15,8 @@ import { ISyncTask } from './types';
15
15
  */
16
16
  export function syncTaskFactory<Input extends any[], Output = any>(log: ILogger, task: (...args: Input) => Promise<Output>, period: number, taskName = 'task'): ISyncTask<Input, Output> {
17
17
 
18
- // Flag that indicates if the task is being executed
19
- let executing = false;
18
+ // Task promise while it is pending. Undefined once the promise is resolved
19
+ let pendingTask: Promise<Output> | undefined;
20
20
  // flag that indicates if the task periodic execution has been started/stopped.
21
21
  let running = false;
22
22
  // Auxiliar counter used to avoid race condition when calling `start` & `stop` intermittently
@@ -26,14 +26,21 @@ export function syncTaskFactory<Input extends any[], Output = any>(log: ILogger,
26
26
  // Id of the periodic call timeout
27
27
  let timeoutID: any;
28
28
 
29
- function execute(...args: Input) {
30
- executing = true;
29
+ function execute(...args: Input): Promise<Output> {
30
+ // If task is executing, chain the new execution
31
+ if (pendingTask) {
32
+ return pendingTask.then(() => {
33
+ return execute(...args);
34
+ });
35
+ }
36
+
37
+ // Execute task
31
38
  log.debug(SYNC_TASK_EXECUTE, [taskName]);
32
- return task(...args).then(result => {
33
- executing = false;
39
+ pendingTask = task(...args).then(result => {
40
+ pendingTask = undefined;
34
41
  return result;
35
42
  });
36
- // No need to handle promise rejection because it is a pre-condition that provided task never rejects.
43
+ return pendingTask;
37
44
  }
38
45
 
39
46
  function periodicExecute(currentRunningId: number) {
@@ -46,11 +53,10 @@ export function syncTaskFactory<Input extends any[], Output = any>(log: ILogger,
46
53
  }
47
54
 
48
55
  return {
49
- // @TODO check if we need to queued `execute` calls, to avoid possible race conditions on submitters and updaters with streaming.
50
56
  execute,
51
57
 
52
58
  isExecuting() {
53
- return executing;
59
+ return pendingTask !== undefined;
54
60
  },
55
61
 
56
62
  start(...args: Input) {
package/src/sync/types.ts CHANGED
@@ -2,6 +2,7 @@ import { IReadinessManager } from '../readiness/types';
2
2
  import { IStorageSync } from '../storages/types';
3
3
  import { IPollingManager } from './polling/types';
4
4
  import { IPushManager } from './streaming/types';
5
+ import { ISubmitterManager } from './submitters/types';
5
6
 
6
7
  export interface ITask<Input extends any[] = []> {
7
8
  /**
@@ -39,7 +40,7 @@ export interface ISyncManager extends ITask {
39
40
  flush(): Promise<any>,
40
41
  pushManager?: IPushManager,
41
42
  pollingManager?: IPollingManager,
42
- submitter?: ISyncTask
43
+ submitterManager?: ISubmitterManager
43
44
  }
44
45
 
45
46
  export interface ISyncManagerCS extends ISyncManager {
package/src/types.ts CHANGED
@@ -117,7 +117,8 @@ export interface ISettings {
117
117
  splitFilters: SplitIO.SplitFilter[],
118
118
  impressionsMode: SplitIO.ImpressionsMode,
119
119
  __splitFiltersValidation: ISplitFiltersValidation,
120
- localhostMode?: SplitIO.LocalhostFactory
120
+ localhostMode?: SplitIO.LocalhostFactory,
121
+ enabled: boolean
121
122
  },
122
123
  readonly runtime: {
123
124
  ip: string | false
@@ -214,6 +215,11 @@ interface ISharedSettings {
214
215
  * @default 'OPTIMIZED'
215
216
  */
216
217
  impressionsMode?: SplitIO.ImpressionsMode,
218
+ /**
219
+ * Enables synchronization.
220
+ * @property {boolean} enabled
221
+ */
222
+ enabled: boolean
217
223
  }
218
224
  }
219
225
  /**
@@ -83,7 +83,8 @@ export const base = {
83
83
  splitFilters: undefined,
84
84
  // impressions collection mode
85
85
  impressionsMode: OPTIMIZED,
86
- localhostMode: undefined
86
+ localhostMode: undefined,
87
+ enabled: true
87
88
  },
88
89
 
89
90
  // Logger
@@ -191,6 +192,11 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
191
192
  scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase);
192
193
  }
193
194
 
195
+ // validate sync enabled
196
+ if (withDefaults.sync.enabled !== false) { // @ts-ignore, modify readonly prop
197
+ withDefaults.sync.enabled = true;
198
+ }
199
+
194
200
  // validate the `splitFilters` settings and parse splits query
195
201
  const splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode);
196
202
  withDefaults.sync.splitFilters = splitFiltersValidation.validFilters;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Auto-require script to use with GaToSplit integration
3
+ */
4
+ export declare function autoRequire(): void;
@@ -1,2 +1,3 @@
1
1
  import { ISdkFactoryContextSync } from '../../sdkFactory/types';
2
- export declare function submitterManagerFactory(params: ISdkFactoryContextSync): import("../types").ISyncTask<[], any>;
2
+ import { ISubmitterManager } from './types';
3
+ export declare function submitterManagerFactory(params: ISdkFactoryContextSync): ISubmitterManager;
@@ -1,5 +1,6 @@
1
1
  import { IMetadata } from '../../dtos/types';
2
2
  import { SplitIO } from '../../types';
3
+ import { ISyncTask } from '../types';
3
4
  export declare type ImpressionsPayload = {
4
5
  /** Split name */
5
6
  f: string;
@@ -169,3 +170,8 @@ export declare type TelemetryConfigStatsPayload = TelemetryConfigStats & {
169
170
  i?: Array<string>;
170
171
  uC: number;
171
172
  };
173
+ export interface ISubmitterManager extends ISyncTask {
174
+ start(onlyTelemetry?: boolean): void;
175
+ stop(allExceptTelemetry?: boolean): void;
176
+ execute(onlyTelemetry?: boolean): Promise<any>;
177
+ }
@@ -2,8 +2,8 @@ import { ILogger } from '../logger/types';
2
2
  import { ISyncTask } from './types';
3
3
  /**
4
4
  * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods).
5
- * The task can be executed once calling the "execute" method.
6
- * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization.
5
+ * The task can be also executed by calling the "execute" method. Multiple execute calls are chained to run secuentially and avoid race conditions.
6
+ * For example, submitters executed on SDK destroy or full queue, while periodic execution is pending.
7
7
  *
8
8
  * @param log Logger instance.
9
9
  * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections.
@@ -2,6 +2,7 @@ import { IReadinessManager } from '../readiness/types';
2
2
  import { IStorageSync } from '../storages/types';
3
3
  import { IPollingManager } from './polling/types';
4
4
  import { IPushManager } from './streaming/types';
5
+ import { ISubmitterManager } from './submitters/types';
5
6
  export interface ITask<Input extends any[] = []> {
6
7
  /**
7
8
  * Start periodic execution of the task
@@ -35,7 +36,7 @@ export interface ISyncManager extends ITask {
35
36
  flush(): Promise<any>;
36
37
  pushManager?: IPushManager;
37
38
  pollingManager?: IPollingManager;
38
- submitter?: ISyncTask;
39
+ submitterManager?: ISubmitterManager;
39
40
  }
40
41
  export interface ISyncManagerCS extends ISyncManager {
41
42
  shared(matchingKey: string, readinessManager: IReadinessManager, storage: IStorageSync): ISyncManager | undefined;
package/types/types.d.ts CHANGED
@@ -112,6 +112,7 @@ export interface ISettings {
112
112
  impressionsMode: SplitIO.ImpressionsMode;
113
113
  __splitFiltersValidation: ISplitFiltersValidation;
114
114
  localhostMode?: SplitIO.LocalhostFactory;
115
+ enabled: boolean;
115
116
  };
116
117
  readonly runtime: {
117
118
  ip: string | false;
@@ -208,6 +209,11 @@ interface ISharedSettings {
208
209
  * @default 'OPTIMIZED'
209
210
  */
210
211
  impressionsMode?: SplitIO.ImpressionsMode;
212
+ /**
213
+ * Enables synchronization.
214
+ * @property {boolean} enabled
215
+ */
216
+ enabled: boolean;
211
217
  };
212
218
  }
213
219
  /**
@@ -37,6 +37,7 @@ export declare const base: {
37
37
  splitFilters: undefined;
38
38
  impressionsMode: string;
39
39
  localhostMode: undefined;
40
+ enabled: boolean;
40
41
  };
41
42
  log: undefined;
42
43
  };
@@ -1,26 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.syncTaskComposite = void 0;
4
- /**
5
- * Composite Sync Task: group of sync tasks that are treated as a single one.
6
- */
7
- function syncTaskComposite(syncTasks) {
8
- return {
9
- start: function () {
10
- syncTasks.forEach(function (syncTask) { return syncTask.start(); });
11
- },
12
- stop: function () {
13
- syncTasks.forEach(function (syncTask) { return syncTask.stop(); });
14
- },
15
- isRunning: function () {
16
- return syncTasks.some(function (syncTask) { return syncTask.isRunning(); });
17
- },
18
- execute: function () {
19
- return Promise.all(syncTasks.map(function (syncTask) { return syncTask.execute(); }));
20
- },
21
- isExecuting: function () {
22
- return syncTasks.some(function (syncTask) { return syncTask.isExecuting(); });
23
- }
24
- };
25
- }
26
- exports.syncTaskComposite = syncTaskComposite;
@@ -1,22 +0,0 @@
1
- /**
2
- * Composite Sync Task: group of sync tasks that are treated as a single one.
3
- */
4
- export function syncTaskComposite(syncTasks) {
5
- return {
6
- start: function () {
7
- syncTasks.forEach(function (syncTask) { return syncTask.start(); });
8
- },
9
- stop: function () {
10
- syncTasks.forEach(function (syncTask) { return syncTask.stop(); });
11
- },
12
- isRunning: function () {
13
- return syncTasks.some(function (syncTask) { return syncTask.isRunning(); });
14
- },
15
- execute: function () {
16
- return Promise.all(syncTasks.map(function (syncTask) { return syncTask.execute(); }));
17
- },
18
- isExecuting: function () {
19
- return syncTasks.some(function (syncTask) { return syncTask.isExecuting(); });
20
- }
21
- };
22
- }
@@ -1,26 +0,0 @@
1
- import { ISyncTask } from './types';
2
-
3
- /**
4
- * Composite Sync Task: group of sync tasks that are treated as a single one.
5
- */
6
- export function syncTaskComposite(syncTasks: ISyncTask[]): ISyncTask {
7
-
8
- return {
9
- start() {
10
- syncTasks.forEach(syncTask => syncTask.start());
11
- },
12
- stop() {
13
- syncTasks.forEach(syncTask => syncTask.stop());
14
- },
15
- isRunning() {
16
- return syncTasks.some(syncTask => syncTask.isRunning());
17
- },
18
- execute() {
19
- return Promise.all(syncTasks.map(syncTask => syncTask.execute()));
20
- },
21
- isExecuting() {
22
- return syncTasks.some(syncTask => syncTask.isExecuting());
23
- }
24
- };
25
-
26
- }