@splitsoftware/splitio-commons 1.17.1-rc.1 → 1.17.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 (73) hide show
  1. package/CHANGES.txt +1 -0
  2. package/cjs/readiness/readinessManager.js +7 -5
  3. package/cjs/sdkClient/sdkClientMethodCS.js +7 -4
  4. package/cjs/sdkClient/sdkClientMethodCSWithTT.js +7 -4
  5. package/cjs/sdkFactory/index.js +33 -10
  6. package/cjs/storages/AbstractSplitsCacheAsync.js +0 -7
  7. package/cjs/storages/AbstractSplitsCacheSync.js +0 -7
  8. package/cjs/storages/dataLoader.js +64 -32
  9. package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +1 -9
  10. package/cjs/storages/inLocalStorage/index.js +6 -1
  11. package/cjs/storages/inMemory/InMemoryStorageCS.js +16 -4
  12. package/cjs/storages/inRedis/RedisAdapter.js +1 -1
  13. package/cjs/storages/pluggable/index.js +37 -32
  14. package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +2 -7
  15. package/cjs/sync/polling/updaters/splitChangesUpdater.js +1 -10
  16. package/cjs/trackers/eventTracker.js +11 -9
  17. package/cjs/trackers/impressionsTracker.js +15 -13
  18. package/cjs/trackers/uniqueKeysTracker.js +5 -3
  19. package/cjs/utils/settingsValidation/storage/storageCS.js +1 -12
  20. package/esm/readiness/readinessManager.js +7 -5
  21. package/esm/sdkClient/sdkClientMethodCS.js +7 -4
  22. package/esm/sdkClient/sdkClientMethodCSWithTT.js +7 -4
  23. package/esm/sdkFactory/index.js +34 -11
  24. package/esm/storages/AbstractSplitsCacheAsync.js +0 -7
  25. package/esm/storages/AbstractSplitsCacheSync.js +0 -7
  26. package/esm/storages/dataLoader.js +61 -30
  27. package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +1 -9
  28. package/esm/storages/inLocalStorage/index.js +7 -2
  29. package/esm/storages/inMemory/InMemoryStorageCS.js +16 -4
  30. package/esm/storages/inRedis/RedisAdapter.js +1 -1
  31. package/esm/storages/pluggable/index.js +37 -32
  32. package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +3 -8
  33. package/esm/sync/polling/updaters/splitChangesUpdater.js +2 -11
  34. package/esm/trackers/eventTracker.js +11 -9
  35. package/esm/trackers/impressionsTracker.js +15 -13
  36. package/esm/trackers/uniqueKeysTracker.js +5 -3
  37. package/esm/utils/settingsValidation/storage/storageCS.js +0 -10
  38. package/package.json +1 -1
  39. package/src/readiness/readinessManager.ts +9 -7
  40. package/src/readiness/types.ts +1 -0
  41. package/src/sdkClient/sdkClientMethodCS.ts +5 -2
  42. package/src/sdkClient/sdkClientMethodCSWithTT.ts +5 -2
  43. package/src/sdkFactory/index.ts +37 -12
  44. package/src/sdkFactory/types.ts +3 -0
  45. package/src/storages/AbstractSplitsCacheAsync.ts +0 -8
  46. package/src/storages/AbstractSplitsCacheSync.ts +0 -8
  47. package/src/storages/dataLoader.ts +62 -32
  48. package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +1 -10
  49. package/src/storages/inLocalStorage/index.ts +8 -2
  50. package/src/storages/inMemory/InMemoryStorageCS.ts +19 -4
  51. package/src/storages/inRedis/RedisAdapter.ts +1 -1
  52. package/src/storages/pluggable/index.ts +38 -33
  53. package/src/storages/types.ts +2 -6
  54. package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +3 -7
  55. package/src/sync/polling/updaters/splitChangesUpdater.ts +3 -11
  56. package/src/trackers/eventTracker.ts +10 -7
  57. package/src/trackers/impressionsTracker.ts +12 -9
  58. package/src/trackers/types.ts +1 -0
  59. package/src/trackers/uniqueKeysTracker.ts +6 -4
  60. package/src/types.ts +9 -8
  61. package/src/utils/settingsValidation/storage/storageCS.ts +0 -13
  62. package/types/readiness/types.d.ts +1 -0
  63. package/types/sdkFactory/types.d.ts +2 -0
  64. package/types/storages/AbstractSplitsCacheAsync.d.ts +0 -5
  65. package/types/storages/AbstractSplitsCacheSync.d.ts +0 -5
  66. package/types/storages/dataLoader.d.ts +17 -6
  67. package/types/storages/inLocalStorage/SplitsCacheInLocal.d.ts +0 -6
  68. package/types/storages/types.d.ts +2 -4
  69. package/types/trackers/eventTracker.d.ts +1 -1
  70. package/types/trackers/impressionsTracker.d.ts +1 -1
  71. package/types/trackers/types.d.ts +1 -0
  72. package/types/types.d.ts +8 -8
  73. package/types/utils/settingsValidation/storage/storageCS.d.ts +0 -5
@@ -1,7 +1,7 @@
1
1
  import { forOwn } from '../../../utils/lang';
2
2
  import { syncTaskFactory } from '../../syncTask';
3
3
  import { CONTROL } from '../../../utils/constants';
4
- import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants';
4
+ import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants';
5
5
  import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants';
6
6
  /**
7
7
  * Offline equivalent of `splitChangesUpdaterFactory`
@@ -43,13 +43,8 @@ export function fromObjectUpdaterFactory(splitsParser, storage, readiness, setti
43
43
  readiness.splits.emit(SDK_SPLITS_ARRIVED);
44
44
  if (startingUp) {
45
45
  startingUp = false;
46
- Promise.resolve(splitsCache.checkCache()).then(function (cacheReady) {
47
- // Emits SDK_READY_FROM_CACHE
48
- if (cacheReady)
49
- readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
50
- // Emits SDK_READY
51
- readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
52
- });
46
+ // Emits SDK_READY
47
+ readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
53
48
  }
54
49
  return true;
55
50
  });
@@ -1,6 +1,6 @@
1
1
  import { _Set, setToArray } from '../../../utils/lang/sets';
2
2
  import { timeout } from '../../../utils/promise/timeout';
3
- import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants';
3
+ import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants';
4
4
  import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants';
5
5
  import { startsWith } from '../../../utils/lang';
6
6
  import { IN_SEGMENT } from '../../../utils/constants';
@@ -121,7 +121,7 @@ export function splitChangesUpdaterFactory(log, splitChangesFetcher, splits, seg
121
121
  function _splitChangesUpdater(since, retry) {
122
122
  if (retry === void 0) { retry = 0; }
123
123
  log.debug(SYNC_SPLITS_FETCH, [since]);
124
- var fetcherPromise = Promise.resolve(splitUpdateNotification ?
124
+ return Promise.resolve(splitUpdateNotification ?
125
125
  { splits: [splitUpdateNotification.payload], till: splitUpdateNotification.changeNumber } :
126
126
  splitChangesFetcher(since, noCache, till, _promiseDecorator))
127
127
  .then(function (splitChanges) {
@@ -165,15 +165,6 @@ export function splitChangesUpdaterFactory(log, splitChangesFetcher, splits, seg
165
165
  }
166
166
  return false;
167
167
  });
168
- // After triggering the requests, if we have cached splits information let's notify that to emit SDK_READY_FROM_CACHE.
169
- // Wrapping in a promise since checkCache can be async.
170
- if (splitsEventEmitter && startingUp) {
171
- Promise.resolve(splits.checkCache()).then(function (isCacheReady) {
172
- if (isCacheReady)
173
- splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED);
174
- });
175
- }
176
- return fetcherPromise;
177
168
  }
178
169
  var sincePromise = Promise.resolve(splits.getChangeNumber()); // `getChangeNumber` never rejects or throws error
179
170
  return sincePromise.then(_splitChangesUpdater);
@@ -9,7 +9,7 @@ import { isConsumerMode } from '../utils/settingsValidation/mode';
9
9
  * @param eventsCache cache to save events
10
10
  * @param integrationsManager optional event handler used for integrations
11
11
  */
12
- export function eventTrackerFactory(settings, eventsCache, integrationsManager, telemetryCache) {
12
+ export function eventTrackerFactory(settings, eventsCache, whenInit, integrationsManager, telemetryCache) {
13
13
  var log = settings.log, mode = settings.mode;
14
14
  var isAsync = isConsumerMode(mode);
15
15
  function queueEventsCallback(eventData, tracked) {
@@ -20,14 +20,16 @@ export function eventTrackerFactory(settings, eventsCache, integrationsManager,
20
20
  log.info(EVENTS_TRACKER_SUCCESS, [msg]);
21
21
  if (integrationsManager) {
22
22
  // Wrap in a timeout because we don't want it to be blocking.
23
- setTimeout(function () {
24
- // copy of event, to avoid unexpected behaviour if modified by integrations
25
- var eventDataCopy = objectAssign({}, eventData);
26
- if (properties)
27
- eventDataCopy.properties = objectAssign({}, properties);
28
- // integrationsManager does not throw errors (they are internally handled by each integration module)
29
- integrationsManager.handleEvent(eventDataCopy);
30
- }, 0);
23
+ whenInit(function () {
24
+ setTimeout(function () {
25
+ // copy of event, to avoid unexpected behaviour if modified by integrations
26
+ var eventDataCopy = objectAssign({}, eventData);
27
+ if (properties)
28
+ eventDataCopy.properties = objectAssign({}, properties);
29
+ // integrationsManager does not throw errors (they are internally handled by each integration module)
30
+ integrationsManager.handleEvent(eventDataCopy);
31
+ });
32
+ });
31
33
  }
32
34
  }
33
35
  else {
@@ -11,7 +11,7 @@ import { CONSENT_DECLINED, DEDUPED, QUEUED } from '../utils/constants';
11
11
  * @param integrationsManager optional integrations manager
12
12
  * @param strategy strategy for impressions tracking.
13
13
  */
14
- export function impressionsTrackerFactory(settings, impressionsCache, strategy, integrationsManager, telemetryCache) {
14
+ export function impressionsTrackerFactory(settings, impressionsCache, strategy, whenInit, integrationsManager, telemetryCache) {
15
15
  var log = settings.log, impressionListener = settings.impressionListener, _a = settings.runtime, ip = _a.ip, hostname = _a.hostname, version = settings.version;
16
16
  return {
17
17
  track: function (impressions, attributes) {
@@ -51,18 +51,20 @@ export function impressionsTrackerFactory(settings, impressionsCache, strategy,
51
51
  sdkLanguageVersion: version
52
52
  };
53
53
  // Wrap in a timeout because we don't want it to be blocking.
54
- setTimeout(function () {
55
- // integrationsManager.handleImpression does not throw errors
56
- if (integrationsManager)
57
- integrationsManager.handleImpression(impressionData);
58
- try { // @ts-ignore. An exception on the listeners should not break the SDK.
59
- if (impressionListener)
60
- impressionListener.logImpression(impressionData);
61
- }
62
- catch (err) {
63
- log.error(ERROR_IMPRESSIONS_LISTENER, [err]);
64
- }
65
- }, 0);
54
+ whenInit(function () {
55
+ setTimeout(function () {
56
+ // integrationsManager.handleImpression does not throw errors
57
+ if (integrationsManager)
58
+ integrationsManager.handleImpression(impressionData);
59
+ try { // @ts-ignore. An exception on the listeners should not break the SDK.
60
+ if (impressionListener)
61
+ impressionListener.logImpression(impressionData);
62
+ }
63
+ catch (err) {
64
+ log.error(ERROR_IMPRESSIONS_LISTENER, [err]);
65
+ }
66
+ });
67
+ });
66
68
  };
67
69
  for (var i = 0; i < impressionsToListenerCount; i++) {
68
70
  _loop_1(i);
@@ -16,9 +16,6 @@ var noopFilterAdapter = {
16
16
  export function uniqueKeysTrackerFactory(log, uniqueKeysCache, filterAdapter) {
17
17
  if (filterAdapter === void 0) { filterAdapter = noopFilterAdapter; }
18
18
  var intervalId;
19
- if (filterAdapter.refreshRate) {
20
- intervalId = setInterval(filterAdapter.clear, filterAdapter.refreshRate);
21
- }
22
19
  return {
23
20
  track: function (key, featureName) {
24
21
  if (!filterAdapter.add(key, featureName)) {
@@ -27,6 +24,11 @@ export function uniqueKeysTrackerFactory(log, uniqueKeysCache, filterAdapter) {
27
24
  }
28
25
  uniqueKeysCache.track(key, featureName);
29
26
  },
27
+ start: function () {
28
+ if (filterAdapter.refreshRate) {
29
+ intervalId = setInterval(filterAdapter.clear, filterAdapter.refreshRate);
30
+ }
31
+ },
30
32
  stop: function () {
31
33
  clearInterval(intervalId);
32
34
  }
@@ -1,12 +1,6 @@
1
1
  import { InMemoryStorageCSFactory } from '../../../storages/inMemory/InMemoryStorageCS';
2
2
  import { ERROR_STORAGE_INVALID } from '../../../logger/constants';
3
3
  import { LOCALHOST_MODE, STANDALONE_MODE, STORAGE_PLUGGABLE, STORAGE_LOCALSTORAGE, STORAGE_MEMORY } from '../../../utils/constants';
4
- export function __InLocalStorageMockFactory(params) {
5
- var result = InMemoryStorageCSFactory(params);
6
- result.splits.checkCache = function () { return true; }; // to emit SDK_READY_FROM_CACHE
7
- return result;
8
- }
9
- __InLocalStorageMockFactory.type = STORAGE_MEMORY;
10
4
  /**
11
5
  * This function validates `settings.storage` object
12
6
  *
@@ -23,10 +17,6 @@ export function validateStorageCS(settings) {
23
17
  storage = InMemoryStorageCSFactory;
24
18
  log.error(ERROR_STORAGE_INVALID);
25
19
  }
26
- // In localhost mode with InLocalStorage, fallback to a mock InLocalStorage to emit SDK_READY_FROM_CACHE
27
- if (mode === LOCALHOST_MODE && storage.type === STORAGE_LOCALSTORAGE) {
28
- return __InLocalStorageMockFactory;
29
- }
30
20
  if ([LOCALHOST_MODE, STANDALONE_MODE].indexOf(mode) === -1) {
31
21
  // Consumer modes require an async storage
32
22
  if (storage.type !== STORAGE_PLUGGABLE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "1.17.1-rc.1",
3
+ "version": "1.17.1-rc.3",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -55,19 +55,15 @@ export function readinessManagerFactory(
55
55
 
56
56
  // emit SDK_READY_TIMED_OUT
57
57
  let hasTimedout = false;
58
+ let readyTimeoutId: ReturnType<typeof setTimeout>;
58
59
 
59
- function timeout() {
60
- if (hasTimedout) return;
60
+ function timeout() { // eslint-disable-next-line no-use-before-define
61
+ if (hasTimedout || isReady) return;
61
62
  hasTimedout = true;
62
63
  syncLastUpdate();
63
64
  gate.emit(SDK_READY_TIMED_OUT, 'Split SDK emitted SDK_READY_TIMED_OUT event.');
64
65
  }
65
66
 
66
- let readyTimeoutId: ReturnType<typeof setTimeout>;
67
- if (readyTimeout > 0) {
68
- readyTimeoutId = setTimeout(timeout, readyTimeout);
69
- }
70
-
71
67
  // emit SDK_READY and SDK_UPDATE
72
68
  let isReady = false;
73
69
  splits.on(SDK_SPLITS_ARRIVED, checkIsReadyOrUpdate);
@@ -132,6 +128,12 @@ export function readinessManagerFactory(
132
128
  // tracking and evaluations, while keeping event listeners to emit SDK_READY_TIMED_OUT event
133
129
  setDestroyed() { isDestroyed = true; },
134
130
 
131
+ init() {
132
+ if (readyTimeout > 0) {
133
+ readyTimeoutId = setTimeout(timeout, readyTimeout);
134
+ }
135
+ },
136
+
135
137
  destroy() {
136
138
  isDestroyed = true;
137
139
  syncLastUpdate();
@@ -59,6 +59,7 @@ export interface IReadinessManager {
59
59
  timeout(): void,
60
60
  setDestroyed(): void,
61
61
  destroy(): void,
62
+ init(): void,
62
63
 
63
64
  /** for client-side */
64
65
  shared(): IReadinessManager,
@@ -15,7 +15,7 @@ import { buildInstanceId } from './identity';
15
15
  * Therefore, clients don't have a bound TT for the track method.
16
16
  */
17
17
  export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: SplitIO.SplitKey) => SplitIO.ICsClient {
18
- const { clients, storage, syncManager, sdkReadinessManager, settings: { core: { key }, log } } = params;
18
+ const { clients, storage, syncManager, sdkReadinessManager, settings: { core: { key }, log }, whenInit } = params;
19
19
 
20
20
  const mainClientInstance = clientCSDecorator(
21
21
  log,
@@ -75,7 +75,10 @@ export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: Spl
75
75
  validKey
76
76
  );
77
77
 
78
- sharedSyncManager && sharedSyncManager.start();
78
+ whenInit(() => {
79
+ sharedSdkReadiness.readinessManager.init();
80
+ sharedSyncManager && sharedSyncManager.start();
81
+ });
79
82
 
80
83
  log.info(NEW_SHARED_CLIENT);
81
84
  } else {
@@ -17,7 +17,7 @@ import { buildInstanceId } from './identity';
17
17
  * (default client) or the client method (shared clients).
18
18
  */
19
19
  export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: SplitIO.SplitKey, trafficType?: string) => SplitIO.ICsClient {
20
- const { clients, storage, syncManager, sdkReadinessManager, settings: { core: { key, trafficType }, log } } = params;
20
+ const { clients, storage, syncManager, sdkReadinessManager, settings: { core: { key, trafficType }, log }, whenInit } = params;
21
21
 
22
22
  const mainClientInstance = clientCSDecorator(
23
23
  log,
@@ -86,7 +86,10 @@ export function sdkClientMethodCSFactory(params: ISdkFactoryContext): (key?: Spl
86
86
  validTrafficType
87
87
  );
88
88
 
89
- sharedSyncManager && sharedSyncManager.start();
89
+ whenInit(() => {
90
+ sharedSdkReadiness.readinessManager.init();
91
+ sharedSyncManager && sharedSyncManager.start();
92
+ });
90
93
 
91
94
  log.info(NEW_SHARED_CLIENT);
92
95
  } else {
@@ -7,7 +7,7 @@ import { IBasicClient, SplitIO } from '../types';
7
7
  import { validateAndTrackApiKey } from '../utils/inputValidation/apiKey';
8
8
  import { createLoggerAPI } from '../logger/sdkLogger';
9
9
  import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants';
10
- import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../readiness/constants';
10
+ import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../readiness/constants';
11
11
  import { objectAssign } from '../utils/lang/objectAssign';
12
12
  import { strategyDebugFactory } from '../trackers/strategy/strategyDebug';
13
13
  import { strategyOptimizedFactory } from '../trackers/strategy/strategyOptimized';
@@ -23,14 +23,20 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
23
23
  const { settings, platform, storageFactory, splitApiFactory, extraProps,
24
24
  syncManagerFactory, SignalListener, impressionsObserverFactory,
25
25
  integrationsManagerFactory, sdkManagerFactory, sdkClientMethodFactory,
26
- filterAdapterFactory } = params;
26
+ filterAdapterFactory, isPure } = params;
27
27
  const { log, sync: { impressionsMode } } = settings;
28
28
 
29
29
  // @TODO handle non-recoverable errors, such as, global `fetch` not available, invalid SDK Key, etc.
30
30
  // On non-recoverable errors, we should mark the SDK as destroyed and not start synchronization.
31
31
 
32
- // We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle.
33
- validateAndTrackApiKey(log, settings.core.authorizationKey);
32
+ // initialization
33
+ let isInit = false;
34
+ const initCallbacks: (() => void)[] = [];
35
+
36
+ function whenInit(cb: () => void) {
37
+ if (isInit) cb();
38
+ else initCallbacks.push(cb);
39
+ }
34
40
 
35
41
  const sdkReadinessManager = sdkReadinessManagerFactory(platform.EventEmitter, settings);
36
42
  const readiness = sdkReadinessManager.readinessManager;
@@ -46,8 +52,11 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
46
52
  readiness.splits.emit(SDK_SPLITS_ARRIVED);
47
53
  readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
48
54
  },
55
+ onReadyFromCacheCb: () => {
56
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
57
+ }
49
58
  });
50
- // @TODO add support for dataloader: `if (params.dataLoader) params.dataLoader(storage);`
59
+
51
60
  const clients: Record<string, IBasicClient> = {};
52
61
  const telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now);
53
62
  const integrationsManager = integrationsManagerFactory && integrationsManagerFactory({ settings, storage, telemetryTracker });
@@ -67,13 +76,13 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
67
76
  strategy = strategyDebugFactory(observer);
68
77
  }
69
78
 
70
- const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, strategy, integrationsManager, storage.telemetry);
71
- const eventTracker = eventTrackerFactory(settings, storage.events, integrationsManager, storage.telemetry);
79
+ const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, strategy, whenInit, integrationsManager, storage.telemetry);
80
+ const eventTracker = eventTrackerFactory(settings, storage.events, whenInit, integrationsManager, storage.telemetry);
72
81
 
73
82
  // splitApi is used by SyncManager and Browser signal listener
74
83
  const splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker);
75
84
 
76
- const ctx: ISdkFactoryContext = { clients, splitApi, eventTracker, impressionsTracker, telemetryTracker, uniqueKeysTracker, sdkReadinessManager, readiness, settings, storage, platform };
85
+ const ctx: ISdkFactoryContext = { clients, splitApi, eventTracker, impressionsTracker, telemetryTracker, uniqueKeysTracker, sdkReadinessManager, readiness, settings, storage, platform, whenInit };
77
86
 
78
87
  const syncManager = syncManagerFactory && syncManagerFactory(ctx as ISdkFactoryContextSync);
79
88
  ctx.syncManager = syncManager;
@@ -85,8 +94,24 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
85
94
  const clientMethod = sdkClientMethodFactory(ctx);
86
95
  const managerInstance = sdkManagerFactory(settings, storage.splits, sdkReadinessManager);
87
96
 
88
- syncManager && syncManager.start();
89
- signalListener && signalListener.start();
97
+
98
+ function init() {
99
+ if (isInit) return;
100
+ isInit = true;
101
+
102
+ // We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle.
103
+ validateAndTrackApiKey(log, settings.core.authorizationKey);
104
+ readiness.init();
105
+ storage.init && storage.init();
106
+ uniqueKeysTracker && uniqueKeysTracker.start();
107
+ syncManager && syncManager.start();
108
+ signalListener && signalListener.start();
109
+
110
+ initCallbacks.forEach((cb) => cb());
111
+ initCallbacks.length = 0;
112
+ }
113
+
114
+ if (!isPure) init();
90
115
 
91
116
  log.info(NEW_FACTORY);
92
117
 
@@ -107,7 +132,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
107
132
  settings,
108
133
 
109
134
  destroy() {
110
- return Promise.all(Object.keys(clients).map(key => clients[key].destroy())).then(() => {});
135
+ return Promise.all(Object.keys(clients).map(key => clients[key].destroy())).then(() => { });
111
136
  }
112
- }, extraProps && extraProps(ctx));
137
+ }, extraProps && extraProps(ctx), isPure && { init });
113
138
  }
@@ -50,6 +50,7 @@ export interface ISdkFactoryContext {
50
50
  splitApi?: ISplitApi
51
51
  syncManager?: ISyncManager,
52
52
  clients: Record<string, IBasicClient>,
53
+ whenInit(cb: () => void): void
53
54
  }
54
55
 
55
56
  export interface ISdkFactoryContextSync extends ISdkFactoryContext {
@@ -68,6 +69,8 @@ export interface ISdkFactoryContextAsync extends ISdkFactoryContext {
68
69
  * Object parameter with the modules required to create an SDK factory instance
69
70
  */
70
71
  export interface ISdkFactoryParams {
72
+ // If true, the `sdkFactory` is pure (no side effects), and the SDK instance includes a `init` method to run initialization side effects
73
+ isPure?: boolean,
71
74
 
72
75
  // The settings must be already validated
73
76
  settings: ISettings,
@@ -28,14 +28,6 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync {
28
28
  return Promise.resolve(true);
29
29
  }
30
30
 
31
- /**
32
- * Check if the splits information is already stored in cache.
33
- * Noop, just keeping the interface. This is used by client-side implementations only.
34
- */
35
- checkCache(): Promise<boolean> {
36
- return Promise.resolve(false);
37
- }
38
-
39
31
  /**
40
32
  * Kill `name` split and set `defaultTreatment` and `changeNumber`.
41
33
  * Used for SPLIT_KILL push notifications.
@@ -48,14 +48,6 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync {
48
48
 
49
49
  abstract clear(): void
50
50
 
51
- /**
52
- * Check if the splits information is already stored in cache. This data can be preloaded.
53
- * It is used as condition to emit SDK_SPLITS_CACHE_LOADED, and then SDK_READY_FROM_CACHE.
54
- */
55
- checkCache(): boolean {
56
- return false;
57
- }
58
-
59
51
  /**
60
52
  * Kill `name` split and set `defaultTreatment` and `changeNumber`.
61
53
  * Used for SPLIT_KILL push notifications.
@@ -1,55 +1,85 @@
1
1
  import { SplitIO } from '../types';
2
- import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../utils/constants/browser';
3
- import { DataLoader, ISegmentsCacheSync, ISplitsCacheSync } from './types';
2
+ import { ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from './types';
3
+ import { setToArray, ISet } from '../utils/lang/sets';
4
+ import { getMatching } from '../utils/key';
4
5
 
5
6
  /**
6
- * Factory of client-side storage loader
7
+ * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function
8
+ * (https://github.com/godaddy/split-javascript-data-loader/blob/master/src/load-data.js)
7
9
  *
8
- * @param preloadedData validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader
9
- * and extended with a `mySegmentsData` property.
10
- * @returns function to preload the storage
10
+ * @param preloadedData validated data following the format proposed in https://github.com/godaddy/split-javascript-data-loader and extended with a `mySegmentsData` property.
11
+ * @param storage object containing `splits` and `segments` cache (client-side variant)
12
+ * @param userKey user key (matching key) of the provided MySegmentsCache
13
+ *
14
+ * @TODO extend to load largeSegments
15
+ * @TODO extend to load data on shared mySegments storages. Be specific when emitting SDK_READY_FROM_CACHE on shared clients. Maybe the serializer should provide the `useSegments` flag.
16
+ * @TODO add logs, and input validation in this module, in favor of size reduction.
17
+ * @TODO unit tests
11
18
  */
12
- export function dataLoaderFactory(preloadedData: SplitIO.PreloadedData): DataLoader {
13
-
14
- /**
15
- * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function
16
- * (https://github.com/godaddy/split-javascript-data-loader/blob/master/src/load-data.js)
17
- *
18
- * @param storage object containing `splits` and `segments` cache (client-side variant)
19
- * @param userId user key string of the provided MySegmentsCache
20
- *
21
- * @TODO extend to support SegmentsCache (server-side variant) by making `userId` optional and adding the corresponding logic.
22
- * @TODO extend to load data on shared mySegments storages. Be specific when emitting SDK_READY_FROM_CACHE on shared clients. Maybe the serializer should provide the `useSegments` flag.
23
- */
24
- return function loadData(storage: { splits: ISplitsCacheSync, segments: ISegmentsCacheSync }, userId: string) {
25
- // Do not load data if current preloadedData is empty
26
- if (Object.keys(preloadedData).length === 0) return;
27
-
28
- const { lastUpdated = -1, segmentsData = {}, since = -1, splitsData = {} } = preloadedData;
19
+ export function loadData(preloadedData: SplitIO.PreloadedData, storage: { splits?: ISplitsCacheSync, segments: ISegmentsCacheSync, largeSegments?: ISegmentsCacheSync }, matchingKey?: string) {
20
+ // Do not load data if current preloadedData is empty
21
+ if (Object.keys(preloadedData).length === 0) return;
22
+
23
+ const { segmentsData = {}, since = -1, splitsData = [] } = preloadedData;
29
24
 
25
+ if (storage.splits) {
30
26
  const storedSince = storage.splits.getChangeNumber();
31
- const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
32
27
 
33
- // Do not load data if current localStorage data is more recent,
34
- // or if its `lastUpdated` timestamp is older than the given `expirationTimestamp`,
35
- if (storedSince > since || lastUpdated < expirationTimestamp) return;
28
+ // Do not load data if current data is more recent
29
+ if (storedSince > since) return;
36
30
 
37
31
  // cleaning up the localStorage data, since some cached splits might need be part of the preloaded data
38
32
  storage.splits.clear();
39
33
  storage.splits.setChangeNumber(since);
40
34
 
41
35
  // splitsData in an object where the property is the split name and the pertaining value is a stringified json of its data
42
- storage.splits.addSplits(Object.keys(splitsData).map(splitName => JSON.parse(splitsData[splitName])));
36
+ storage.splits.addSplits(splitsData.map(split => ([split.name, split])));
37
+ }
43
38
 
44
- // add mySegments data
45
- let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId];
39
+ if (matchingKey) { // add mySegments data (client-side)
40
+ let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[matchingKey];
46
41
  if (!mySegmentsData) {
47
42
  // segmentsData in an object where the property is the segment name and the pertaining value is a stringified object that contains the `added` array of userIds
48
43
  mySegmentsData = Object.keys(segmentsData).filter(segmentName => {
49
- const userIds = JSON.parse(segmentsData[segmentName]).added;
50
- return Array.isArray(userIds) && userIds.indexOf(userId) > -1;
44
+ const matchingKeys = segmentsData[segmentName];
45
+ return matchingKeys.indexOf(matchingKey) > -1;
51
46
  });
52
47
  }
53
48
  storage.segments.resetSegments({ k: mySegmentsData.map(s => ({ n: s })) });
49
+ } else { // add segments data (server-side)
50
+ Object.keys(segmentsData).filter(segmentName => {
51
+ const matchingKeys = segmentsData[segmentName];
52
+ storage.segments.addToSegment(segmentName, matchingKeys);
53
+ });
54
+ }
55
+ }
56
+
57
+ export function getSnapshot(storage: IStorageSync, userKeys?: SplitIO.SplitKey[]): SplitIO.PreloadedData {
58
+ return {
59
+ // lastUpdated: Date.now(),
60
+ since: storage.splits.getChangeNumber(),
61
+ splitsData: storage.splits.getAll(),
62
+ segmentsData: userKeys ?
63
+ undefined : // @ts-ignore accessing private prop
64
+ Object.keys(storage.segments.segmentCache).reduce((prev, cur) => { // @ts-ignore accessing private prop
65
+ prev[cur] = setToArray(storage.segments.segmentCache[cur] as ISet<string>);
66
+ return prev;
67
+ }, {}),
68
+ mySegmentsData: userKeys ?
69
+ userKeys.reduce<Record<string, string[]>>((prev, userKey) => {
70
+ prev[getMatching(userKey)] = storage.shared ?
71
+ // Client-side segments
72
+ // @ts-ignore accessing private prop
73
+ Object.keys(storage.shared(userKey).segments.segmentCache) :
74
+ // Server-side segments
75
+ // @ts-ignore accessing private prop
76
+ Object.keys(storage.segments.segmentCache).reduce<string[]>((prev, segmentName) => { // @ts-ignore accessing private prop
77
+ return storage.segments.segmentCache[segmentName].has(userKey) ?
78
+ prev.concat(segmentName) :
79
+ prev;
80
+ }, []);
81
+ return prev;
82
+ }, {}) :
83
+ undefined
54
84
  };
55
85
  }
@@ -217,15 +217,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
217
217
  }
218
218
  }
219
219
 
220
- /**
221
- * Check if the splits information is already stored in browser LocalStorage.
222
- * In this function we could add more code to check if the data is valid.
223
- * @override
224
- */
225
- checkCache(): boolean {
226
- return this.getChangeNumber() > -1;
227
- }
228
-
229
220
  /**
230
221
  * Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`,
231
222
  *
@@ -250,7 +241,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
250
241
  this.updateNewFilter = true;
251
242
 
252
243
  // if there is cache, clear it
253
- if (this.checkCache()) this.clear();
244
+ if (this.getChangeNumber() > -1) this.clear();
254
245
 
255
246
  } catch (e) {
256
247
  this.log.error(LOG_PREFIX + e);
@@ -12,7 +12,7 @@ import { SplitsCacheInMemory } from '../inMemory/SplitsCacheInMemory';
12
12
  import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser';
13
13
  import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS';
14
14
  import { LOG_PREFIX } from './constants';
15
- import { DEBUG, NONE, STORAGE_LOCALSTORAGE } from '../../utils/constants';
15
+ import { DEBUG, LOCALHOST_MODE, NONE, STORAGE_LOCALSTORAGE } from '../../utils/constants';
16
16
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory';
17
17
  import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
18
18
  import { getMatching } from '../../utils/key';
@@ -36,7 +36,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
36
36
  return InMemoryStorageCSFactory(params);
37
37
  }
38
38
 
39
- const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params;
39
+ const { onReadyFromCacheCb, settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params;
40
40
  const matchingKey = getMatching(settings.core.key);
41
41
  const keys = new KeyBuilderCS(prefix, matchingKey);
42
42
  const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
@@ -55,6 +55,12 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
55
55
  telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
56
56
  uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,
57
57
 
58
+ init() {
59
+ if (settings.mode === LOCALHOST_MODE || splits.getChangeNumber() > -1) {
60
+ Promise.resolve().then(onReadyFromCacheCb);
61
+ }
62
+ },
63
+
58
64
  destroy() {
59
65
  this.splits = new SplitsCacheInMemory(__splitFiltersValidation);
60
66
  this.segments = new MySegmentsCacheInMemory();