@splitsoftware/splitio-commons 1.17.1-rc.2 → 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 (45) hide show
  1. package/cjs/readiness/readinessManager.js +7 -5
  2. package/cjs/sdkClient/sdkClientMethodCS.js +7 -4
  3. package/cjs/sdkClient/sdkClientMethodCSWithTT.js +7 -4
  4. package/cjs/sdkFactory/index.js +30 -9
  5. package/cjs/storages/dataLoader.js +10 -11
  6. package/cjs/storages/inLocalStorage/index.js +5 -3
  7. package/cjs/storages/inRedis/RedisAdapter.js +1 -1
  8. package/cjs/storages/pluggable/index.js +37 -32
  9. package/cjs/trackers/eventTracker.js +11 -9
  10. package/cjs/trackers/impressionsTracker.js +15 -13
  11. package/cjs/trackers/uniqueKeysTracker.js +5 -3
  12. package/esm/readiness/readinessManager.js +7 -5
  13. package/esm/sdkClient/sdkClientMethodCS.js +7 -4
  14. package/esm/sdkClient/sdkClientMethodCSWithTT.js +7 -4
  15. package/esm/sdkFactory/index.js +30 -9
  16. package/esm/storages/dataLoader.js +10 -11
  17. package/esm/storages/inLocalStorage/index.js +5 -3
  18. package/esm/storages/inRedis/RedisAdapter.js +1 -1
  19. package/esm/storages/pluggable/index.js +37 -32
  20. package/esm/trackers/eventTracker.js +11 -9
  21. package/esm/trackers/impressionsTracker.js +15 -13
  22. package/esm/trackers/uniqueKeysTracker.js +5 -3
  23. package/package.json +1 -1
  24. package/src/readiness/readinessManager.ts +9 -7
  25. package/src/readiness/types.ts +1 -0
  26. package/src/sdkClient/sdkClientMethodCS.ts +5 -2
  27. package/src/sdkClient/sdkClientMethodCSWithTT.ts +5 -2
  28. package/src/sdkFactory/index.ts +32 -10
  29. package/src/sdkFactory/types.ts +3 -0
  30. package/src/storages/dataLoader.ts +12 -13
  31. package/src/storages/inLocalStorage/index.ts +6 -4
  32. package/src/storages/inRedis/RedisAdapter.ts +1 -1
  33. package/src/storages/pluggable/index.ts +38 -33
  34. package/src/storages/types.ts +1 -0
  35. package/src/trackers/eventTracker.ts +10 -7
  36. package/src/trackers/impressionsTracker.ts +12 -9
  37. package/src/trackers/types.ts +1 -0
  38. package/src/trackers/uniqueKeysTracker.ts +6 -4
  39. package/types/readiness/types.d.ts +1 -0
  40. package/types/sdkFactory/types.d.ts +2 -0
  41. package/types/storages/dataLoader.d.ts +2 -2
  42. package/types/storages/types.d.ts +1 -0
  43. package/types/trackers/eventTracker.d.ts +1 -1
  44. package/types/trackers/impressionsTracker.d.ts +1 -1
  45. package/types/trackers/types.d.ts +1 -0
@@ -1,4 +1,5 @@
1
1
  import { setToArray } from '../utils/lang/sets';
2
+ import { getMatching } from '../utils/key';
2
3
  /**
3
4
  * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function
4
5
  * (https://github.com/godaddy/split-javascript-data-loader/blob/master/src/load-data.js)
@@ -12,7 +13,7 @@ import { setToArray } from '../utils/lang/sets';
12
13
  * @TODO add logs, and input validation in this module, in favor of size reduction.
13
14
  * @TODO unit tests
14
15
  */
15
- export function loadData(preloadedData, storage, userKey) {
16
+ export function loadData(preloadedData, storage, matchingKey) {
16
17
  // Do not load data if current preloadedData is empty
17
18
  if (Object.keys(preloadedData).length === 0)
18
19
  return;
@@ -28,29 +29,28 @@ export function loadData(preloadedData, storage, userKey) {
28
29
  // splitsData in an object where the property is the split name and the pertaining value is a stringified json of its data
29
30
  storage.splits.addSplits(splitsData.map(function (split) { return ([split.name, split]); }));
30
31
  }
31
- if (userKey) { // add mySegments data (client-side)
32
- var mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userKey];
32
+ if (matchingKey) { // add mySegments data (client-side)
33
+ var mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[matchingKey];
33
34
  if (!mySegmentsData) {
34
35
  // 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
35
36
  mySegmentsData = Object.keys(segmentsData).filter(function (segmentName) {
36
- var userKeys = segmentsData[segmentName];
37
- return userKeys.indexOf(userKey) > -1;
37
+ var matchingKeys = segmentsData[segmentName];
38
+ return matchingKeys.indexOf(matchingKey) > -1;
38
39
  });
39
40
  }
40
41
  storage.segments.resetSegments({ k: mySegmentsData.map(function (s) { return ({ n: s }); }) });
41
42
  }
42
43
  else { // add segments data (server-side)
43
44
  Object.keys(segmentsData).filter(function (segmentName) {
44
- var userKeys = segmentsData[segmentName];
45
- storage.segments.addToSegment(segmentName, userKeys);
45
+ var matchingKeys = segmentsData[segmentName];
46
+ storage.segments.addToSegment(segmentName, matchingKeys);
46
47
  });
47
48
  }
48
49
  }
49
50
  export function getSnapshot(storage, userKeys) {
50
51
  return {
51
52
  // lastUpdated: Date.now(),
52
- // @ts-ignore accessing private prop
53
- since: storage.splits.changeNumber,
53
+ since: storage.splits.getChangeNumber(),
54
54
  splitsData: storage.splits.getAll(),
55
55
  segmentsData: userKeys ?
56
56
  undefined : // @ts-ignore accessing private prop
@@ -60,8 +60,7 @@ export function getSnapshot(storage, userKeys) {
60
60
  }, {}),
61
61
  mySegmentsData: userKeys ?
62
62
  userKeys.reduce(function (prev, userKey) {
63
- // @ts-ignore accessing private prop
64
- prev[userKey] = storage.shared ?
63
+ prev[getMatching(userKey)] = storage.shared ?
65
64
  // Client-side segments
66
65
  // @ts-ignore accessing private prop
67
66
  Object.keys(storage.shared(userKey).segments.segmentCache) :
@@ -34,9 +34,6 @@ export function InLocalStorage(options) {
34
34
  var splits = new SplitsCacheInLocal(settings, keys, expirationTimestamp);
35
35
  var segments = new MySegmentsCacheInLocal(log, keys);
36
36
  var largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey));
37
- if (settings.mode === LOCALHOST_MODE || splits.getChangeNumber() > -1) {
38
- Promise.resolve().then(onReadyFromCacheCb);
39
- }
40
37
  return {
41
38
  splits: splits,
42
39
  segments: segments,
@@ -46,6 +43,11 @@ export function InLocalStorage(options) {
46
43
  events: new EventsCacheInMemory(eventsQueueSize),
47
44
  telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
48
45
  uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,
46
+ init: function () {
47
+ if (settings.mode === LOCALHOST_MODE || splits.getChangeNumber() > -1) {
48
+ Promise.resolve().then(onReadyFromCacheCb);
49
+ }
50
+ },
49
51
  destroy: function () {
50
52
  var _a;
51
53
  this.splits = new SplitsCacheInMemory(__splitFiltersValidation);
@@ -17,7 +17,7 @@ var DEFAULT_OPTIONS = {
17
17
  var DEFAULT_LIBRARY_OPTIONS = {
18
18
  enableOfflineQueue: false,
19
19
  connectTimeout: DEFAULT_OPTIONS.connectionTimeout,
20
- lazyConnect: false
20
+ lazyConnect: false // @TODO true to avoid side-effects on instantiation.
21
21
  };
22
22
  /**
23
23
  * Redis adapter on top of the library of choice (written with ioredis) for some extra control.
@@ -55,6 +55,7 @@ export function PluggableStorage(options) {
55
55
  var metadata = metadataBuilder(settings);
56
56
  var keys = new KeyBuilderSS(prefix, metadata);
57
57
  var wrapper = wrapperAdapter(log, options.wrapper);
58
+ var connectPromise;
58
59
  var isSyncronizer = mode === undefined; // If mode is not defined, the synchronizer is running
59
60
  var isPartialConsumer = mode === CONSUMER_PARTIAL_MODE;
60
61
  var telemetry = shouldRecordTelemetry(params) || isSyncronizer ?
@@ -72,37 +73,6 @@ export function PluggableStorage(options) {
72
73
  settings.core.key === undefined ? new UniqueKeysCacheInMemory() : new UniqueKeysCacheInMemoryCS() :
73
74
  new UniqueKeysCachePluggable(log, keys.buildUniqueKeysKey(), wrapper) :
74
75
  undefined;
75
- // Connects to wrapper and emits SDK_READY event on main client
76
- var connectPromise = wrapper.connect().then(function () {
77
- if (isSyncronizer) {
78
- // In standalone or producer mode, clear storage if SDK key or feature flag filter has changed
79
- return wrapper.get(keys.buildHashKey()).then(function (hash) {
80
- var currentHash = getStorageHash(settings);
81
- if (hash !== currentHash) {
82
- log.info(LOG_PREFIX + 'Storage HASH has changed (SDK key, flags filter criteria or flags spec version was modified). Clearing cache');
83
- return wrapper.getKeysByPrefix(keys.prefix + ".").then(function (storageKeys) {
84
- return Promise.all(storageKeys.map(function (storageKey) { return wrapper.del(storageKey); }));
85
- }).then(function () { return wrapper.set(keys.buildHashKey(), currentHash); });
86
- }
87
- }).then(function () {
88
- onReadyCb();
89
- });
90
- }
91
- else {
92
- // Start periodic flush of async storages if not running synchronizer (producer mode)
93
- if (impressionCountsCache && impressionCountsCache.start)
94
- impressionCountsCache.start();
95
- if (uniqueKeysCache && uniqueKeysCache.start)
96
- uniqueKeysCache.start();
97
- if (telemetry && telemetry.recordConfig)
98
- telemetry.recordConfig();
99
- onReadyCb();
100
- }
101
- }).catch(function (e) {
102
- e = e || new Error('Error connecting wrapper');
103
- onReadyCb(e);
104
- return e; // Propagate error for shared clients
105
- });
106
76
  return {
107
77
  splits: new SplitsCachePluggable(log, keys, wrapper, settings.sync.__splitFiltersValidation),
108
78
  segments: new SegmentsCachePluggable(log, keys, wrapper),
@@ -111,6 +81,41 @@ export function PluggableStorage(options) {
111
81
  events: isPartialConsumer ? promisifyEventsTrack(new EventsCacheInMemory(eventsQueueSize)) : new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata),
112
82
  telemetry: telemetry,
113
83
  uniqueKeys: uniqueKeysCache,
84
+ init: function () {
85
+ if (connectPromise)
86
+ return connectPromise;
87
+ // Connects to wrapper and emits SDK_READY event on main client
88
+ return connectPromise = wrapper.connect().then(function () {
89
+ if (isSyncronizer) {
90
+ // In standalone or producer mode, clear storage if SDK key or feature flag filter has changed
91
+ return wrapper.get(keys.buildHashKey()).then(function (hash) {
92
+ var currentHash = getStorageHash(settings);
93
+ if (hash !== currentHash) {
94
+ log.info(LOG_PREFIX + 'Storage HASH has changed (SDK key, flags filter criteria or flags spec version was modified). Clearing cache');
95
+ return wrapper.getKeysByPrefix(keys.prefix + ".").then(function (storageKeys) {
96
+ return Promise.all(storageKeys.map(function (storageKey) { return wrapper.del(storageKey); }));
97
+ }).then(function () { return wrapper.set(keys.buildHashKey(), currentHash); });
98
+ }
99
+ }).then(function () {
100
+ onReadyCb();
101
+ });
102
+ }
103
+ else {
104
+ // Start periodic flush of async storages if not running synchronizer (producer mode)
105
+ if (impressionCountsCache && impressionCountsCache.start)
106
+ impressionCountsCache.start();
107
+ if (uniqueKeysCache && uniqueKeysCache.start)
108
+ uniqueKeysCache.start();
109
+ if (telemetry && telemetry.recordConfig)
110
+ telemetry.recordConfig();
111
+ onReadyCb();
112
+ }
113
+ }).catch(function (e) {
114
+ e = e || new Error('Error connecting wrapper');
115
+ onReadyCb(e);
116
+ return e; // Propagate error for shared clients
117
+ });
118
+ },
114
119
  // Stop periodic flush and disconnect the underlying storage
115
120
  destroy: function () {
116
121
  return Promise.all(isSyncronizer ? [] : [
@@ -120,7 +125,7 @@ export function PluggableStorage(options) {
120
125
  },
121
126
  // emits SDK_READY event on shared clients and returns a reference to the storage
122
127
  shared: function (_, onReadyCb) {
123
- connectPromise.then(onReadyCb);
128
+ this.init().then(onReadyCb);
124
129
  return __assign(__assign({}, this), {
125
130
  // no-op destroy, to disconnect the wrapper only when the main client is destroyed
126
131
  destroy: function () { } });
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "1.17.1-rc.2",
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 {
@@ -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;
@@ -70,13 +76,13 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
70
76
  strategy = strategyDebugFactory(observer);
71
77
  }
72
78
 
73
- const impressionsTracker = impressionsTrackerFactory(settings, storage.impressions, strategy, integrationsManager, storage.telemetry);
74
- 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);
75
81
 
76
82
  // splitApi is used by SyncManager and Browser signal listener
77
83
  const splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker);
78
84
 
79
- 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 };
80
86
 
81
87
  const syncManager = syncManagerFactory && syncManagerFactory(ctx as ISdkFactoryContextSync);
82
88
  ctx.syncManager = syncManager;
@@ -88,8 +94,24 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
88
94
  const clientMethod = sdkClientMethodFactory(ctx);
89
95
  const managerInstance = sdkManagerFactory(settings, storage.splits, sdkReadinessManager);
90
96
 
91
- syncManager && syncManager.start();
92
- 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();
93
115
 
94
116
  log.info(NEW_FACTORY);
95
117
 
@@ -110,7 +132,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
110
132
  settings,
111
133
 
112
134
  destroy() {
113
- 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(() => { });
114
136
  }
115
- }, extraProps && extraProps(ctx));
137
+ }, extraProps && extraProps(ctx), isPure && { init });
116
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,
@@ -1,6 +1,7 @@
1
1
  import { SplitIO } from '../types';
2
2
  import { ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from './types';
3
3
  import { setToArray, ISet } from '../utils/lang/sets';
4
+ import { getMatching } from '../utils/key';
4
5
 
5
6
  /**
6
7
  * Storage-agnostic adaptation of `loadDataIntoLocalStorage` function
@@ -15,7 +16,7 @@ import { setToArray, ISet } from '../utils/lang/sets';
15
16
  * @TODO add logs, and input validation in this module, in favor of size reduction.
16
17
  * @TODO unit tests
17
18
  */
18
- export function loadData(preloadedData: SplitIO.PreloadedData, storage: { splits?: ISplitsCacheSync, segments: ISegmentsCacheSync, largeSegments?: ISegmentsCacheSync }, userKey?: string) {
19
+ export function loadData(preloadedData: SplitIO.PreloadedData, storage: { splits?: ISplitsCacheSync, segments: ISegmentsCacheSync, largeSegments?: ISegmentsCacheSync }, matchingKey?: string) {
19
20
  // Do not load data if current preloadedData is empty
20
21
  if (Object.keys(preloadedData).length === 0) return;
21
22
 
@@ -35,29 +36,28 @@ export function loadData(preloadedData: SplitIO.PreloadedData, storage: { splits
35
36
  storage.splits.addSplits(splitsData.map(split => ([split.name, split])));
36
37
  }
37
38
 
38
- if (userKey) { // add mySegments data (client-side)
39
- let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userKey];
39
+ if (matchingKey) { // add mySegments data (client-side)
40
+ let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[matchingKey];
40
41
  if (!mySegmentsData) {
41
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
42
43
  mySegmentsData = Object.keys(segmentsData).filter(segmentName => {
43
- const userKeys = segmentsData[segmentName];
44
- return userKeys.indexOf(userKey) > -1;
44
+ const matchingKeys = segmentsData[segmentName];
45
+ return matchingKeys.indexOf(matchingKey) > -1;
45
46
  });
46
47
  }
47
48
  storage.segments.resetSegments({ k: mySegmentsData.map(s => ({ n: s })) });
48
49
  } else { // add segments data (server-side)
49
50
  Object.keys(segmentsData).filter(segmentName => {
50
- const userKeys = segmentsData[segmentName];
51
- storage.segments.addToSegment(segmentName, userKeys);
51
+ const matchingKeys = segmentsData[segmentName];
52
+ storage.segments.addToSegment(segmentName, matchingKeys);
52
53
  });
53
54
  }
54
55
  }
55
56
 
56
- export function getSnapshot(storage: IStorageSync, userKeys?: string[]): SplitIO.PreloadedData {
57
+ export function getSnapshot(storage: IStorageSync, userKeys?: SplitIO.SplitKey[]): SplitIO.PreloadedData {
57
58
  return {
58
59
  // lastUpdated: Date.now(),
59
- // @ts-ignore accessing private prop
60
- since: storage.splits.changeNumber,
60
+ since: storage.splits.getChangeNumber(),
61
61
  splitsData: storage.splits.getAll(),
62
62
  segmentsData: userKeys ?
63
63
  undefined : // @ts-ignore accessing private prop
@@ -66,9 +66,8 @@ export function getSnapshot(storage: IStorageSync, userKeys?: string[]): SplitIO
66
66
  return prev;
67
67
  }, {}),
68
68
  mySegmentsData: userKeys ?
69
- userKeys.reduce((prev, userKey) => {
70
- // @ts-ignore accessing private prop
71
- prev[userKey] = storage.shared ?
69
+ userKeys.reduce<Record<string, string[]>>((prev, userKey) => {
70
+ prev[getMatching(userKey)] = storage.shared ?
72
71
  // Client-side segments
73
72
  // @ts-ignore accessing private prop
74
73
  Object.keys(storage.shared(userKey).segments.segmentCache) :
@@ -45,10 +45,6 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
45
45
  const segments = new MySegmentsCacheInLocal(log, keys);
46
46
  const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey));
47
47
 
48
- if (settings.mode === LOCALHOST_MODE || splits.getChangeNumber() > -1) {
49
- Promise.resolve().then(onReadyFromCacheCb);
50
- }
51
-
52
48
  return {
53
49
  splits,
54
50
  segments,
@@ -59,6 +55,12 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn
59
55
  telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
60
56
  uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined,
61
57
 
58
+ init() {
59
+ if (settings.mode === LOCALHOST_MODE || splits.getChangeNumber() > -1) {
60
+ Promise.resolve().then(onReadyFromCacheCb);
61
+ }
62
+ },
63
+
62
64
  destroy() {
63
65
  this.splits = new SplitsCacheInMemory(__splitFiltersValidation);
64
66
  this.segments = new MySegmentsCacheInMemory();
@@ -20,7 +20,7 @@ const DEFAULT_OPTIONS = {
20
20
  const DEFAULT_LIBRARY_OPTIONS = {
21
21
  enableOfflineQueue: false,
22
22
  connectTimeout: DEFAULT_OPTIONS.connectionTimeout,
23
- lazyConnect: false
23
+ lazyConnect: false // @TODO true to avoid side-effects on instantiation.
24
24
  };
25
25
 
26
26
  interface IRedisCommand {