@splitsoftware/splitio-commons 2.10.2-rc.3 → 2.10.2-rc.5

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 (40) hide show
  1. package/CHANGES.txt +1 -1
  2. package/cjs/readiness/constants.js +4 -1
  3. package/cjs/readiness/readinessManager.js +13 -13
  4. package/cjs/sdkFactory/index.js +3 -3
  5. package/cjs/storages/inLocalStorage/index.js +4 -1
  6. package/cjs/storages/inLocalStorage/validateCache.js +13 -3
  7. package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +6 -6
  8. package/cjs/sync/polling/types.js +0 -15
  9. package/cjs/sync/polling/updaters/mySegmentsUpdater.js +1 -2
  10. package/cjs/sync/polling/updaters/segmentChangesUpdater.js +6 -6
  11. package/cjs/sync/polling/updaters/splitChangesUpdater.js +6 -6
  12. package/cjs/sync/syncManagerOnline.js +4 -3
  13. package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
  14. package/esm/readiness/constants.js +3 -0
  15. package/esm/readiness/readinessManager.js +13 -13
  16. package/esm/sdkFactory/index.js +3 -3
  17. package/esm/storages/inLocalStorage/index.js +4 -1
  18. package/esm/storages/inLocalStorage/validateCache.js +13 -3
  19. package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +7 -7
  20. package/esm/sync/polling/types.js +1 -14
  21. package/esm/sync/polling/updaters/mySegmentsUpdater.js +2 -3
  22. package/esm/sync/polling/updaters/segmentChangesUpdater.js +7 -7
  23. package/esm/sync/polling/updaters/splitChangesUpdater.js +7 -7
  24. package/esm/sync/syncManagerOnline.js +4 -3
  25. package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
  26. package/package.json +1 -1
  27. package/src/readiness/constants.ts +4 -0
  28. package/src/readiness/readinessManager.ts +14 -13
  29. package/src/sdkFactory/index.ts +3 -3
  30. package/src/storages/inLocalStorage/index.ts +6 -3
  31. package/src/storages/inLocalStorage/validateCache.ts +20 -4
  32. package/src/storages/types.ts +1 -1
  33. package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +7 -6
  34. package/src/sync/polling/types.ts +0 -14
  35. package/src/sync/polling/updaters/mySegmentsUpdater.ts +2 -3
  36. package/src/sync/polling/updaters/segmentChangesUpdater.ts +8 -7
  37. package/src/sync/polling/updaters/splitChangesUpdater.ts +8 -7
  38. package/src/sync/syncManagerOnline.ts +4 -2
  39. package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
  40. package/types/splitio.d.ts +1 -1
package/CHANGES.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  2.11.0 (January XX, 2026)
2
2
  - Added metadata to SDK_UPDATE events to indicate the type of update (FLAGS_UPDATE or SEGMENTS_UPDATE) and the names of updated flags or segments.
3
- - Added metadata to SDK_READY and SDK_READY_FROM_CACHE events, including `initialCacheLoad` (boolean indicating if SDK was loaded from cache) and `lastUpdateTimestamp` (Int64 milliseconds since epoch).
3
+ - Added metadata to SDK_READY and SDK_READY_FROM_CACHE events, including `initialCacheLoad` (boolean: `true` for fresh install/first app launch, `false` for warm cache/second app launch) and `lastUpdateTimestamp` (Int64 milliseconds since epoch).
4
4
 
5
5
  2.10.1 (December 18, 2025)
6
6
  - Bugfix - Handle `null` prerequisites properly.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SDK_UPDATE = exports.SDK_READY_FROM_CACHE = exports.SDK_READY = exports.SDK_READY_TIMED_OUT = exports.SDK_SEGMENTS_ARRIVED = exports.SDK_SPLITS_CACHE_LOADED = exports.SDK_SPLITS_ARRIVED = void 0;
3
+ exports.SEGMENTS_UPDATE = exports.FLAGS_UPDATE = exports.SDK_UPDATE = exports.SDK_READY_FROM_CACHE = exports.SDK_READY = exports.SDK_READY_TIMED_OUT = exports.SDK_SEGMENTS_ARRIVED = exports.SDK_SPLITS_CACHE_LOADED = exports.SDK_SPLITS_ARRIVED = void 0;
4
4
  // Splits events:
5
5
  exports.SDK_SPLITS_ARRIVED = 'state::splits-arrived';
6
6
  exports.SDK_SPLITS_CACHE_LOADED = 'state::splits-cache-loaded';
@@ -11,3 +11,6 @@ exports.SDK_READY_TIMED_OUT = 'init::timeout';
11
11
  exports.SDK_READY = 'init::ready';
12
12
  exports.SDK_READY_FROM_CACHE = 'init::cache-ready';
13
13
  exports.SDK_UPDATE = 'state::update';
14
+ // SdkUpdateMetadata types:
15
+ exports.FLAGS_UPDATE = 'FLAGS_UPDATE';
16
+ exports.SEGMENTS_UPDATE = 'SEGMENTS_UPDATE';
@@ -41,6 +41,7 @@ function readinessManagerFactory(EventEmitter, settings, splits, isShared) {
41
41
  }
42
42
  // emit SDK_READY_FROM_CACHE
43
43
  var isReadyFromCache = false;
44
+ var cacheLastUpdateTimestamp = null;
44
45
  if (splits.splitsCacheLoaded)
45
46
  isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE
46
47
  else
@@ -68,17 +69,17 @@ function readinessManagerFactory(EventEmitter, settings, splits, isShared) {
68
69
  splits.initCallbacks.push(__init);
69
70
  if (splits.hasInit)
70
71
  __init();
71
- function checkIsReadyFromCache() {
72
+ function checkIsReadyFromCache(cacheMetadata) {
72
73
  isReadyFromCache = true;
74
+ cacheLastUpdateTimestamp = cacheMetadata.lastUpdateTimestamp;
73
75
  // Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
74
76
  if (!isReady && !isDestroyed) {
75
77
  try {
76
78
  syncLastUpdate();
77
- var metadata = {
78
- initialCacheLoad: true,
79
- lastUpdateTimestamp: lastUpdate
80
- };
81
- gate.emit(constants_1.SDK_READY_FROM_CACHE, metadata);
79
+ gate.emit(constants_1.SDK_READY_FROM_CACHE, {
80
+ initialCacheLoad: !cacheMetadata.isCacheValid,
81
+ lastUpdateTimestamp: cacheLastUpdateTimestamp
82
+ });
82
83
  }
83
84
  catch (e) {
84
85
  // throws user callback exceptions in next tick
@@ -104,19 +105,18 @@ function readinessManagerFactory(EventEmitter, settings, splits, isShared) {
104
105
  clearTimeout(readyTimeoutId);
105
106
  isReady = true;
106
107
  try {
107
- syncLastUpdate();
108
108
  var wasReadyFromCache = isReadyFromCache;
109
109
  if (!isReadyFromCache) {
110
110
  isReadyFromCache = true;
111
- var metadataFromCache = {
112
- initialCacheLoad: false,
113
- lastUpdateTimestamp: lastUpdate
111
+ var metadataReadyFromCache = {
112
+ initialCacheLoad: true,
113
+ lastUpdateTimestamp: null // No cache timestamp when fresh install
114
114
  };
115
- gate.emit(constants_1.SDK_READY_FROM_CACHE, metadataFromCache);
115
+ gate.emit(constants_1.SDK_READY_FROM_CACHE, metadataReadyFromCache);
116
116
  }
117
117
  var metadataReady = {
118
- initialCacheLoad: wasReadyFromCache,
119
- lastUpdateTimestamp: lastUpdate
118
+ initialCacheLoad: !wasReadyFromCache,
119
+ lastUpdateTimestamp: wasReadyFromCache ? cacheLastUpdateTimestamp : null
120
120
  };
121
121
  gate.emit(constants_1.SDK_READY, metadataReady);
122
122
  }
@@ -46,17 +46,17 @@ function sdkFactory(params) {
46
46
  return;
47
47
  }
48
48
  readiness.splits.emit(constants_2.SDK_SPLITS_ARRIVED);
49
- readiness.segments.emit(constants_2.SDK_SEGMENTS_ARRIVED);
49
+ readiness.segments.emit(constants_2.SDK_SEGMENTS_ARRIVED, { isCacheValid: true, lastUpdateTimestamp: null });
50
50
  },
51
51
  onReadyFromCacheCb: function () {
52
- readiness.splits.emit(constants_2.SDK_SPLITS_CACHE_LOADED);
52
+ readiness.splits.emit(constants_2.SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null });
53
53
  }
54
54
  });
55
55
  var fallbackTreatmentsCalculator = new fallbackTreatmentsCalculator_1.FallbackTreatmentsCalculator(settings.fallbackTreatments);
56
56
  if (initialRolloutPlan) {
57
57
  (0, setRolloutPlan_1.setRolloutPlan)(log, initialRolloutPlan, storage, key && (0, key_1.getMatching)(key));
58
58
  if (storage.splits.getChangeNumber() > -1)
59
- readiness.splits.emit(constants_2.SDK_SPLITS_CACHE_LOADED);
59
+ readiness.splits.emit(constants_2.SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null });
60
60
  }
61
61
  var clients = {};
62
62
  var telemetryTracker = (0, telemetryTracker_1.telemetryTrackerFactory)(storage.telemetry, platform.now);
@@ -60,7 +60,10 @@ function InLocalStorage(options) {
60
60
  telemetry: (0, TelemetryCacheInMemory_1.shouldRecordTelemetry)(params) ? new TelemetryCacheInMemory_1.TelemetryCacheInMemory(splits, segments) : undefined,
61
61
  uniqueKeys: new UniqueKeysCacheInMemoryCS_1.UniqueKeysCacheInMemoryCS(),
62
62
  validateCache: function () {
63
- return validateCachePromise || (validateCachePromise = (0, validateCache_1.validateCache)(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
63
+ if (!validateCachePromise) {
64
+ validateCachePromise = (0, validateCache_1.validateCache)(options, storage, settings, keys, splits, rbSegments, segments, largeSegments);
65
+ }
66
+ return validateCachePromise;
64
67
  },
65
68
  save: function () {
66
69
  return storage.save && storage.save();
@@ -55,12 +55,16 @@ function validateExpiration(options, storage, settings, keys, currentTimestamp,
55
55
  * - its hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified
56
56
  * - `clearOnInit` was set and cache was not cleared in the last 24 hours
57
57
  *
58
- * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
58
+ * @returns Metadata object with `isCacheValid` (true if cache is ready to be used, false otherwise) and `lastUpdateTimestamp` (timestamp of last cache update or null)
59
59
  */
60
60
  function validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments) {
61
61
  return Promise.resolve(storage.load && storage.load()).then(function () {
62
62
  var currentTimestamp = Date.now();
63
63
  var isThereCache = splits.getChangeNumber() > -1;
64
+ // Get lastUpdateTimestamp from storage
65
+ var lastUpdatedTimestampStr = storage.getItem(keys.buildLastUpdatedKey());
66
+ var lastUpdatedTimestamp = lastUpdatedTimestampStr ? parseInt(lastUpdatedTimestampStr, 10) : null;
67
+ var lastUpdateTimestamp = (!(0, lang_1.isNaNNumber)(lastUpdatedTimestamp) && lastUpdatedTimestamp !== null) ? lastUpdatedTimestamp : null;
64
68
  if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) {
65
69
  splits.clear();
66
70
  rbSegments.clear();
@@ -76,10 +80,16 @@ function validateCache(options, storage, settings, keys, splits, rbSegments, seg
76
80
  // Persist clear
77
81
  if (storage.save)
78
82
  storage.save();
79
- return false;
83
+ return {
84
+ isCacheValid: false,
85
+ lastUpdateTimestamp: null
86
+ };
80
87
  }
81
88
  // Check if ready from cache
82
- return isThereCache;
89
+ return {
90
+ isCacheValid: isThereCache,
91
+ lastUpdateTimestamp: lastUpdateTimestamp
92
+ };
83
93
  });
84
94
  }
85
95
  exports.validateCache = validateCache;
@@ -6,7 +6,6 @@ var syncTask_1 = require("../../syncTask");
6
6
  var constants_1 = require("../../../utils/constants");
7
7
  var constants_2 = require("../../../readiness/constants");
8
8
  var constants_3 = require("../../../logger/constants");
9
- var types_1 = require("../../polling/types");
10
9
  /**
11
10
  * Offline equivalent of `splitChangesUpdaterFactory`
12
11
  */
@@ -43,15 +42,16 @@ function fromObjectUpdaterFactory(splitsParser, storage, readiness, settings) {
43
42
  splitsCache.clear(),
44
43
  splitsCache.update(splits, [], Date.now())
45
44
  ]).then(function () {
46
- readiness.splits.emit(constants_2.SDK_SPLITS_ARRIVED, { type: types_1.SdkUpdateMetadataKeys.FLAGS_UPDATE, names: [] });
45
+ readiness.splits.emit(constants_2.SDK_SPLITS_ARRIVED, { type: constants_2.FLAGS_UPDATE, names: [] });
47
46
  if (startingUp) {
48
47
  startingUp = false;
49
- Promise.resolve(storage.validateCache ? storage.validateCache() : false).then(function (isCacheLoaded) {
48
+ Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then(function (cacheMetadata) {
50
49
  // Emits SDK_READY_FROM_CACHE
51
- if (isCacheLoaded)
52
- readiness.splits.emit(constants_2.SDK_SPLITS_CACHE_LOADED);
50
+ if (cacheMetadata.isCacheValid) {
51
+ readiness.splits.emit(constants_2.SDK_SPLITS_CACHE_LOADED, cacheMetadata);
52
+ }
53
53
  // Emits SDK_READY
54
- readiness.segments.emit(constants_2.SDK_SEGMENTS_ARRIVED, { type: types_1.SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
54
+ readiness.segments.emit(constants_2.SDK_SEGMENTS_ARRIVED, { type: constants_2.SEGMENTS_UPDATE, names: [] });
55
55
  });
56
56
  }
57
57
  return true;
@@ -1,17 +1,2 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SdkUpdateMetadataKeys = void 0;
4
- /**
5
- * Metadata keys for SDK update events.
6
- */
7
- var SdkUpdateMetadataKeys;
8
- (function (SdkUpdateMetadataKeys) {
9
- /**
10
- * The update event emitted when the SDK cache is updated with new data for flags.
11
- */
12
- SdkUpdateMetadataKeys["FLAGS_UPDATE"] = "FLAGS_UPDATE";
13
- /**
14
- * The update event emitted when the SDK cache is updated with new data for segments.
15
- */
16
- SdkUpdateMetadataKeys["SEGMENTS_UPDATE"] = "SEGMENTS_UPDATE";
17
- })(SdkUpdateMetadataKeys = exports.SdkUpdateMetadataKeys || (exports.SdkUpdateMetadataKeys = {}));
@@ -6,7 +6,6 @@ var constants_1 = require("../../../readiness/constants");
6
6
  var constants_2 = require("../../../logger/constants");
7
7
  var constants_3 = require("../../streaming/constants");
8
8
  var AbstractSplitsCacheSync_1 = require("../../../storages/AbstractSplitsCacheSync");
9
- var types_1 = require("../types");
10
9
  /**
11
10
  * factory of MySegments updater, a task that:
12
11
  * - fetches mySegments using `mySegmentsFetcher`
@@ -40,7 +39,7 @@ function mySegmentsUpdaterFactory(log, mySegmentsFetcher, storage, segmentsEvent
40
39
  // Notify update if required
41
40
  if ((0, AbstractSplitsCacheSync_1.usesSegmentsSync)(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
42
41
  readyOnAlreadyExistentState = false;
43
- segmentsEventEmitter.emit(constants_1.SDK_SEGMENTS_ARRIVED, { type: types_1.SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
42
+ segmentsEventEmitter.emit(constants_1.SDK_SEGMENTS_ARRIVED, { type: constants_1.SEGMENTS_UPDATE, names: [] });
44
43
  }
45
44
  }
46
45
  function _mySegmentsUpdater(retry, segmentsData, noCache, till) {
@@ -4,7 +4,6 @@ exports.segmentChangesUpdaterFactory = void 0;
4
4
  var constants_1 = require("../../../readiness/constants");
5
5
  var constants_2 = require("../../../logger/constants");
6
6
  var timeout_1 = require("../../../utils/promise/timeout");
7
- var types_1 = require("../types");
8
7
  /**
9
8
  * Factory of SegmentChanges updater, a task that:
10
9
  * - fetches segment changes using `segmentChangesFetcher`
@@ -67,12 +66,13 @@ function segmentChangesUpdaterFactory(log, segmentChangesFetcher, segments, read
67
66
  // if at least one segment fetch succeeded, mark segments ready
68
67
  if (shouldUpdateFlags.some(function (update) { return update; }) || readyOnAlreadyExistentState) {
69
68
  readyOnAlreadyExistentState = false;
70
- var metadata = {
71
- type: types_1.SdkUpdateMetadataKeys.SEGMENTS_UPDATE,
72
- names: []
73
- };
74
- if (readiness)
69
+ if (readiness) {
70
+ var metadata = {
71
+ type: constants_1.SEGMENTS_UPDATE,
72
+ names: []
73
+ };
75
74
  readiness.segments.emit(constants_1.SDK_SEGMENTS_ARRIVED, metadata);
75
+ }
76
76
  }
77
77
  return true;
78
78
  });
@@ -8,7 +8,6 @@ var lang_1 = require("../../../utils/lang");
8
8
  var constants_3 = require("../../../utils/constants");
9
9
  var sets_1 = require("../../../utils/lang/sets");
10
10
  var constants_4 = require("../../streaming/constants");
11
- var types_1 = require("../types");
12
11
  // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment).
13
12
  // Returns a promise that could be rejected.
14
13
  // @TODO review together with Segments and MySegments storage APIs
@@ -165,13 +164,14 @@ function splitChangesUpdaterFactory(log, splitChangesFetcher, storage, splitFilt
165
164
  return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
166
165
  .catch(function () { return false; } /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
167
166
  .then(function (emitSplitsArrivedEvent) {
168
- var metadata = {
169
- type: updatedFlags.length > 0 ? types_1.SdkUpdateMetadataKeys.FLAGS_UPDATE : types_1.SdkUpdateMetadataKeys.SEGMENTS_UPDATE,
170
- names: updatedFlags.length > 0 ? updatedFlags : []
171
- };
172
167
  // emit SDK events
173
- if (emitSplitsArrivedEvent)
168
+ if (emitSplitsArrivedEvent) {
169
+ var metadata = {
170
+ type: updatedFlags.length > 0 ? constants_1.FLAGS_UPDATE : constants_1.SEGMENTS_UPDATE,
171
+ names: updatedFlags.length > 0 ? updatedFlags : []
172
+ };
174
173
  splitsEventEmitter.emit(constants_1.SDK_SPLITS_ARRIVED, metadata);
174
+ }
175
175
  return true;
176
176
  });
177
177
  }
@@ -71,13 +71,14 @@ function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
71
71
  running = true;
72
72
  // @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved
73
73
  submitterManager.start(!(0, consent_1.isConsentGranted)(settings));
74
- return Promise.resolve(storage.validateCache ? storage.validateCache() : false).then(function (isCacheLoaded) {
74
+ return Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then(function (cacheMetadata) {
75
75
  if (!running)
76
76
  return;
77
77
  if (startFirstTime) {
78
78
  // Emits SDK_READY_FROM_CACHE
79
- if (isCacheLoaded)
80
- readiness.splits.emit(constants_4.SDK_SPLITS_CACHE_LOADED);
79
+ if (cacheMetadata.isCacheValid) {
80
+ readiness.splits.emit(constants_4.SDK_SPLITS_CACHE_LOADED, cacheMetadata);
81
+ }
81
82
  }
82
83
  // start syncing splits and segments
83
84
  if (pollingManager) {
@@ -6,7 +6,7 @@ var constants_1 = require("../../../logger/constants");
6
6
  var constants_2 = require("../../../utils/constants");
7
7
  function __InLocalStorageMockFactory(params) {
8
8
  var result = (0, InMemoryStorageCS_1.InMemoryStorageCSFactory)(params);
9
- result.validateCache = function () { return Promise.resolve(true); }; // to emit SDK_READY_FROM_CACHE
9
+ result.validateCache = function () { return Promise.resolve({ isCacheValid: true, lastUpdateTimestamp: null }); }; // to emit SDK_READY_FROM_CACHE
10
10
  return result;
11
11
  }
12
12
  exports.__InLocalStorageMockFactory = __InLocalStorageMockFactory;
@@ -8,3 +8,6 @@ export var SDK_READY_TIMED_OUT = 'init::timeout';
8
8
  export var SDK_READY = 'init::ready';
9
9
  export var SDK_READY_FROM_CACHE = 'init::cache-ready';
10
10
  export var SDK_UPDATE = 'state::update';
11
+ // SdkUpdateMetadata types:
12
+ export var FLAGS_UPDATE = 'FLAGS_UPDATE';
13
+ export var SEGMENTS_UPDATE = 'SEGMENTS_UPDATE';
@@ -38,6 +38,7 @@ export function readinessManagerFactory(EventEmitter, settings, splits, isShared
38
38
  }
39
39
  // emit SDK_READY_FROM_CACHE
40
40
  var isReadyFromCache = false;
41
+ var cacheLastUpdateTimestamp = null;
41
42
  if (splits.splitsCacheLoaded)
42
43
  isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE
43
44
  else
@@ -65,17 +66,17 @@ export function readinessManagerFactory(EventEmitter, settings, splits, isShared
65
66
  splits.initCallbacks.push(__init);
66
67
  if (splits.hasInit)
67
68
  __init();
68
- function checkIsReadyFromCache() {
69
+ function checkIsReadyFromCache(cacheMetadata) {
69
70
  isReadyFromCache = true;
71
+ cacheLastUpdateTimestamp = cacheMetadata.lastUpdateTimestamp;
70
72
  // Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
71
73
  if (!isReady && !isDestroyed) {
72
74
  try {
73
75
  syncLastUpdate();
74
- var metadata = {
75
- initialCacheLoad: true,
76
- lastUpdateTimestamp: lastUpdate
77
- };
78
- gate.emit(SDK_READY_FROM_CACHE, metadata);
76
+ gate.emit(SDK_READY_FROM_CACHE, {
77
+ initialCacheLoad: !cacheMetadata.isCacheValid,
78
+ lastUpdateTimestamp: cacheLastUpdateTimestamp
79
+ });
79
80
  }
80
81
  catch (e) {
81
82
  // throws user callback exceptions in next tick
@@ -101,19 +102,18 @@ export function readinessManagerFactory(EventEmitter, settings, splits, isShared
101
102
  clearTimeout(readyTimeoutId);
102
103
  isReady = true;
103
104
  try {
104
- syncLastUpdate();
105
105
  var wasReadyFromCache = isReadyFromCache;
106
106
  if (!isReadyFromCache) {
107
107
  isReadyFromCache = true;
108
- var metadataFromCache = {
109
- initialCacheLoad: false,
110
- lastUpdateTimestamp: lastUpdate
108
+ var metadataReadyFromCache = {
109
+ initialCacheLoad: true,
110
+ lastUpdateTimestamp: null // No cache timestamp when fresh install
111
111
  };
112
- gate.emit(SDK_READY_FROM_CACHE, metadataFromCache);
112
+ gate.emit(SDK_READY_FROM_CACHE, metadataReadyFromCache);
113
113
  }
114
114
  var metadataReady = {
115
- initialCacheLoad: wasReadyFromCache,
116
- lastUpdateTimestamp: lastUpdate
115
+ initialCacheLoad: !wasReadyFromCache,
116
+ lastUpdateTimestamp: wasReadyFromCache ? cacheLastUpdateTimestamp : null
117
117
  };
118
118
  gate.emit(SDK_READY, metadataReady);
119
119
  }
@@ -43,17 +43,17 @@ export function sdkFactory(params) {
43
43
  return;
44
44
  }
45
45
  readiness.splits.emit(SDK_SPLITS_ARRIVED);
46
- readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
46
+ readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { isCacheValid: true, lastUpdateTimestamp: null });
47
47
  },
48
48
  onReadyFromCacheCb: function () {
49
- readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
49
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null });
50
50
  }
51
51
  });
52
52
  var fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.fallbackTreatments);
53
53
  if (initialRolloutPlan) {
54
54
  setRolloutPlan(log, initialRolloutPlan, storage, key && getMatching(key));
55
55
  if (storage.splits.getChangeNumber() > -1)
56
- readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
56
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null });
57
57
  }
58
58
  var clients = {};
59
59
  var telemetryTracker = telemetryTrackerFactory(storage.telemetry, platform.now);
@@ -57,7 +57,10 @@ export function InLocalStorage(options) {
57
57
  telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
58
58
  uniqueKeys: new UniqueKeysCacheInMemoryCS(),
59
59
  validateCache: function () {
60
- return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
60
+ if (!validateCachePromise) {
61
+ validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments);
62
+ }
63
+ return validateCachePromise;
61
64
  },
62
65
  save: function () {
63
66
  return storage.save && storage.save();
@@ -52,12 +52,16 @@ function validateExpiration(options, storage, settings, keys, currentTimestamp,
52
52
  * - its hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified
53
53
  * - `clearOnInit` was set and cache was not cleared in the last 24 hours
54
54
  *
55
- * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
55
+ * @returns Metadata object with `isCacheValid` (true if cache is ready to be used, false otherwise) and `lastUpdateTimestamp` (timestamp of last cache update or null)
56
56
  */
57
57
  export function validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments) {
58
58
  return Promise.resolve(storage.load && storage.load()).then(function () {
59
59
  var currentTimestamp = Date.now();
60
60
  var isThereCache = splits.getChangeNumber() > -1;
61
+ // Get lastUpdateTimestamp from storage
62
+ var lastUpdatedTimestampStr = storage.getItem(keys.buildLastUpdatedKey());
63
+ var lastUpdatedTimestamp = lastUpdatedTimestampStr ? parseInt(lastUpdatedTimestampStr, 10) : null;
64
+ var lastUpdateTimestamp = (!isNaNNumber(lastUpdatedTimestamp) && lastUpdatedTimestamp !== null) ? lastUpdatedTimestamp : null;
61
65
  if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) {
62
66
  splits.clear();
63
67
  rbSegments.clear();
@@ -73,9 +77,15 @@ export function validateCache(options, storage, settings, keys, splits, rbSegmen
73
77
  // Persist clear
74
78
  if (storage.save)
75
79
  storage.save();
76
- return false;
80
+ return {
81
+ isCacheValid: false,
82
+ lastUpdateTimestamp: null
83
+ };
77
84
  }
78
85
  // Check if ready from cache
79
- return isThereCache;
86
+ return {
87
+ isCacheValid: isThereCache,
88
+ lastUpdateTimestamp: lastUpdateTimestamp
89
+ };
80
90
  });
81
91
  }
@@ -1,9 +1,8 @@
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, SDK_SPLITS_CACHE_LOADED, FLAGS_UPDATE, SEGMENTS_UPDATE } from '../../../readiness/constants';
5
5
  import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants';
6
- import { SdkUpdateMetadataKeys } from '../../polling/types';
7
6
  /**
8
7
  * Offline equivalent of `splitChangesUpdaterFactory`
9
8
  */
@@ -40,15 +39,16 @@ export function fromObjectUpdaterFactory(splitsParser, storage, readiness, setti
40
39
  splitsCache.clear(),
41
40
  splitsCache.update(splits, [], Date.now())
42
41
  ]).then(function () {
43
- readiness.splits.emit(SDK_SPLITS_ARRIVED, { type: SdkUpdateMetadataKeys.FLAGS_UPDATE, names: [] });
42
+ readiness.splits.emit(SDK_SPLITS_ARRIVED, { type: FLAGS_UPDATE, names: [] });
44
43
  if (startingUp) {
45
44
  startingUp = false;
46
- Promise.resolve(storage.validateCache ? storage.validateCache() : false).then(function (isCacheLoaded) {
45
+ Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then(function (cacheMetadata) {
47
46
  // Emits SDK_READY_FROM_CACHE
48
- if (isCacheLoaded)
49
- readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
47
+ if (cacheMetadata.isCacheValid) {
48
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, cacheMetadata);
49
+ }
50
50
  // Emits SDK_READY
51
- readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
51
+ readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
52
52
  });
53
53
  }
54
54
  return true;
@@ -1,14 +1 @@
1
- /**
2
- * Metadata keys for SDK update events.
3
- */
4
- export var SdkUpdateMetadataKeys;
5
- (function (SdkUpdateMetadataKeys) {
6
- /**
7
- * The update event emitted when the SDK cache is updated with new data for flags.
8
- */
9
- SdkUpdateMetadataKeys["FLAGS_UPDATE"] = "FLAGS_UPDATE";
10
- /**
11
- * The update event emitted when the SDK cache is updated with new data for segments.
12
- */
13
- SdkUpdateMetadataKeys["SEGMENTS_UPDATE"] = "SEGMENTS_UPDATE";
14
- })(SdkUpdateMetadataKeys || (SdkUpdateMetadataKeys = {}));
1
+ export {};
@@ -1,9 +1,8 @@
1
1
  import { timeout } from '../../../utils/promise/timeout';
2
- import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants';
2
+ import { SDK_SEGMENTS_ARRIVED, SEGMENTS_UPDATE } from '../../../readiness/constants';
3
3
  import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants';
4
4
  import { MEMBERSHIPS_LS_UPDATE } from '../../streaming/constants';
5
5
  import { usesSegmentsSync } from '../../../storages/AbstractSplitsCacheSync';
6
- import { SdkUpdateMetadataKeys } from '../types';
7
6
  /**
8
7
  * factory of MySegments updater, a task that:
9
8
  * - fetches mySegments using `mySegmentsFetcher`
@@ -37,7 +36,7 @@ export function mySegmentsUpdaterFactory(log, mySegmentsFetcher, storage, segmen
37
36
  // Notify update if required
38
37
  if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
39
38
  readyOnAlreadyExistentState = false;
40
- segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
39
+ segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
41
40
  }
42
41
  }
43
42
  function _mySegmentsUpdater(retry, segmentsData, noCache, till) {
@@ -1,7 +1,6 @@
1
- import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants';
1
+ import { SDK_SEGMENTS_ARRIVED, SEGMENTS_UPDATE } from '../../../readiness/constants';
2
2
  import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants';
3
3
  import { timeout } from '../../../utils/promise/timeout';
4
- import { SdkUpdateMetadataKeys } from '../types';
5
4
  /**
6
5
  * Factory of SegmentChanges updater, a task that:
7
6
  * - fetches segment changes using `segmentChangesFetcher`
@@ -64,12 +63,13 @@ export function segmentChangesUpdaterFactory(log, segmentChangesFetcher, segment
64
63
  // if at least one segment fetch succeeded, mark segments ready
65
64
  if (shouldUpdateFlags.some(function (update) { return update; }) || readyOnAlreadyExistentState) {
66
65
  readyOnAlreadyExistentState = false;
67
- var metadata = {
68
- type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE,
69
- names: []
70
- };
71
- if (readiness)
66
+ if (readiness) {
67
+ var metadata = {
68
+ type: SEGMENTS_UPDATE,
69
+ names: []
70
+ };
72
71
  readiness.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);
72
+ }
73
73
  }
74
74
  return true;
75
75
  });
@@ -1,11 +1,10 @@
1
1
  import { timeout } from '../../../utils/promise/timeout';
2
- import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants';
2
+ import { SDK_SPLITS_ARRIVED, FLAGS_UPDATE, SEGMENTS_UPDATE } from '../../../readiness/constants';
3
3
  import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_RBS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants';
4
4
  import { startsWith } from '../../../utils/lang';
5
5
  import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants';
6
6
  import { setToArray } from '../../../utils/lang/sets';
7
7
  import { SPLIT_UPDATE } from '../../streaming/constants';
8
- import { SdkUpdateMetadataKeys } from '../types';
9
8
  // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment).
10
9
  // Returns a promise that could be rejected.
11
10
  // @TODO review together with Segments and MySegments storage APIs
@@ -160,13 +159,14 @@ export function splitChangesUpdaterFactory(log, splitChangesFetcher, storage, sp
160
159
  return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
161
160
  .catch(function () { return false; } /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
162
161
  .then(function (emitSplitsArrivedEvent) {
163
- var metadata = {
164
- type: updatedFlags.length > 0 ? SdkUpdateMetadataKeys.FLAGS_UPDATE : SdkUpdateMetadataKeys.SEGMENTS_UPDATE,
165
- names: updatedFlags.length > 0 ? updatedFlags : []
166
- };
167
162
  // emit SDK events
168
- if (emitSplitsArrivedEvent)
163
+ if (emitSplitsArrivedEvent) {
164
+ var metadata = {
165
+ type: updatedFlags.length > 0 ? FLAGS_UPDATE : SEGMENTS_UPDATE,
166
+ names: updatedFlags.length > 0 ? updatedFlags : []
167
+ };
169
168
  splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, metadata);
169
+ }
170
170
  return true;
171
171
  });
172
172
  }
@@ -68,13 +68,14 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
68
68
  running = true;
69
69
  // @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved
70
70
  submitterManager.start(!isConsentGranted(settings));
71
- return Promise.resolve(storage.validateCache ? storage.validateCache() : false).then(function (isCacheLoaded) {
71
+ return Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then(function (cacheMetadata) {
72
72
  if (!running)
73
73
  return;
74
74
  if (startFirstTime) {
75
75
  // Emits SDK_READY_FROM_CACHE
76
- if (isCacheLoaded)
77
- readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
76
+ if (cacheMetadata.isCacheValid) {
77
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, cacheMetadata);
78
+ }
78
79
  }
79
80
  // start syncing splits and segments
80
81
  if (pollingManager) {
@@ -3,7 +3,7 @@ import { ERROR_STORAGE_INVALID } from '../../../logger/constants';
3
3
  import { LOCALHOST_MODE, STANDALONE_MODE, STORAGE_PLUGGABLE, STORAGE_LOCALSTORAGE, STORAGE_MEMORY } from '../../../utils/constants';
4
4
  export function __InLocalStorageMockFactory(params) {
5
5
  var result = InMemoryStorageCSFactory(params);
6
- result.validateCache = function () { return Promise.resolve(true); }; // to emit SDK_READY_FROM_CACHE
6
+ result.validateCache = function () { return Promise.resolve({ isCacheValid: true, lastUpdateTimestamp: null }); }; // to emit SDK_READY_FROM_CACHE
7
7
  return result;
8
8
  }
9
9
  __InLocalStorageMockFactory.type = STORAGE_MEMORY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "2.10.2-rc.3",
3
+ "version": "2.10.2-rc.5",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -10,3 +10,7 @@ export const SDK_READY_TIMED_OUT = 'init::timeout';
10
10
  export const SDK_READY = 'init::ready';
11
11
  export const SDK_READY_FROM_CACHE = 'init::cache-ready';
12
12
  export const SDK_UPDATE = 'state::update';
13
+
14
+ // SdkUpdateMetadata types:
15
+ export const FLAGS_UPDATE = 'FLAGS_UPDATE';
16
+ export const SEGMENTS_UPDATE = 'SEGMENTS_UPDATE';
@@ -3,6 +3,7 @@ import { ISettings } from '../types';
3
3
  import SplitIO from '../../types/splitio';
4
4
  import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants';
5
5
  import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types';
6
+ import { CacheValidationMetadata } from '../storages/inLocalStorage/validateCache';
6
7
 
7
8
  function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter {
8
9
  const splitsEventEmitter = objectAssign(new EventEmitter(), {
@@ -55,6 +56,7 @@ export function readinessManagerFactory(
55
56
 
56
57
  // emit SDK_READY_FROM_CACHE
57
58
  let isReadyFromCache = false;
59
+ let cacheLastUpdateTimestamp: number | null = null;
58
60
  if (splits.splitsCacheLoaded) isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE
59
61
  else splits.once(SDK_SPLITS_CACHE_LOADED, checkIsReadyFromCache);
60
62
 
@@ -84,17 +86,17 @@ export function readinessManagerFactory(
84
86
  splits.initCallbacks.push(__init);
85
87
  if (splits.hasInit) __init();
86
88
 
87
- function checkIsReadyFromCache() {
89
+ function checkIsReadyFromCache(cacheMetadata: CacheValidationMetadata) {
88
90
  isReadyFromCache = true;
91
+ cacheLastUpdateTimestamp = cacheMetadata.lastUpdateTimestamp;
89
92
  // Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
90
93
  if (!isReady && !isDestroyed) {
91
94
  try {
92
95
  syncLastUpdate();
93
- const metadata: SplitIO.SdkReadyMetadata = {
94
- initialCacheLoad: true,
95
- lastUpdateTimestamp: lastUpdate
96
- };
97
- gate.emit(SDK_READY_FROM_CACHE, metadata);
96
+ gate.emit(SDK_READY_FROM_CACHE, {
97
+ initialCacheLoad: !cacheMetadata.isCacheValid,
98
+ lastUpdateTimestamp: cacheLastUpdateTimestamp
99
+ });
98
100
  } catch (e) {
99
101
  // throws user callback exceptions in next tick
100
102
  setTimeout(() => { throw e; }, 0);
@@ -117,19 +119,18 @@ export function readinessManagerFactory(
117
119
  clearTimeout(readyTimeoutId);
118
120
  isReady = true;
119
121
  try {
120
- syncLastUpdate();
121
122
  const wasReadyFromCache = isReadyFromCache;
122
123
  if (!isReadyFromCache) {
123
124
  isReadyFromCache = true;
124
- const metadataFromCache: SplitIO.SdkReadyMetadata = {
125
- initialCacheLoad: false,
126
- lastUpdateTimestamp: lastUpdate
125
+ const metadataReadyFromCache: SplitIO.SdkReadyMetadata = {
126
+ initialCacheLoad: true,
127
+ lastUpdateTimestamp: null // No cache timestamp when fresh install
127
128
  };
128
- gate.emit(SDK_READY_FROM_CACHE, metadataFromCache);
129
+ gate.emit(SDK_READY_FROM_CACHE, metadataReadyFromCache);
129
130
  }
130
131
  const metadataReady: SplitIO.SdkReadyMetadata = {
131
- initialCacheLoad: wasReadyFromCache,
132
- lastUpdateTimestamp: lastUpdate
132
+ initialCacheLoad: !wasReadyFromCache,
133
+ lastUpdateTimestamp: wasReadyFromCache ? cacheLastUpdateTimestamp : null
133
134
  };
134
135
  gate.emit(SDK_READY, metadataReady);
135
136
  } catch (e) {
@@ -54,10 +54,10 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
54
54
  return;
55
55
  }
56
56
  readiness.splits.emit(SDK_SPLITS_ARRIVED);
57
- readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
57
+ readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { isCacheValid: true, lastUpdateTimestamp: null });
58
58
  },
59
59
  onReadyFromCacheCb() {
60
- readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
60
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null });
61
61
  }
62
62
  });
63
63
 
@@ -65,7 +65,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
65
65
 
66
66
  if (initialRolloutPlan) {
67
67
  setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key));
68
- if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
68
+ if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { isCacheValid: true, lastUpdateTimestamp: null });
69
69
  }
70
70
 
71
71
  const clients: Record<string, SplitIO.IBasicClient> = {};
@@ -14,7 +14,7 @@ import { STORAGE_LOCALSTORAGE } from '../../utils/constants';
14
14
  import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory';
15
15
  import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
16
16
  import { getMatching } from '../../utils/key';
17
- import { validateCache } from './validateCache';
17
+ import { validateCache, CacheValidationMetadata } from './validateCache';
18
18
  import { ILogger } from '../../logger/types';
19
19
  import SplitIO from '../../../types/splitio';
20
20
  import { storageAdapter } from './storageAdapter';
@@ -54,7 +54,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
54
54
  const rbSegments = new RBSegmentsCacheInLocal(settings, keys, storage);
55
55
  const segments = new MySegmentsCacheInLocal(log, keys, storage);
56
56
  const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage);
57
- let validateCachePromise: Promise<boolean> | undefined;
57
+ let validateCachePromise: Promise<CacheValidationMetadata> | undefined;
58
58
 
59
59
  return {
60
60
  splits,
@@ -68,7 +68,10 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
68
68
  uniqueKeys: new UniqueKeysCacheInMemoryCS(),
69
69
 
70
70
  validateCache() {
71
- return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
71
+ if (!validateCachePromise) {
72
+ validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments);
73
+ }
74
+ return validateCachePromise;
72
75
  },
73
76
 
74
77
  save() {
@@ -12,6 +12,11 @@ import { StorageAdapter } from '../types';
12
12
  const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10;
13
13
  const MILLIS_IN_A_DAY = 86400000;
14
14
 
15
+ export interface CacheValidationMetadata {
16
+ isCacheValid: boolean;
17
+ lastUpdateTimestamp: number | null;
18
+ }
19
+
15
20
  /**
16
21
  * Validates if cache should be cleared and sets the cache `hash` if needed.
17
22
  *
@@ -66,14 +71,19 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: Sto
66
71
  * - its hash has changed, i.e., the SDK key, flags filter criteria or flags spec version was modified
67
72
  * - `clearOnInit` was set and cache was not cleared in the last 24 hours
68
73
  *
69
- * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
74
+ * @returns Metadata object with `isCacheValid` (true if cache is ready to be used, false otherwise) and `lastUpdateTimestamp` (timestamp of last cache update or null)
70
75
  */
71
- export function validateCache(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise<boolean> {
76
+ export function validateCache(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise<CacheValidationMetadata> {
72
77
 
73
78
  return Promise.resolve(storage.load && storage.load()).then(() => {
74
79
  const currentTimestamp = Date.now();
75
80
  const isThereCache = splits.getChangeNumber() > -1;
76
81
 
82
+ // Get lastUpdateTimestamp from storage
83
+ const lastUpdatedTimestampStr = storage.getItem(keys.buildLastUpdatedKey());
84
+ const lastUpdatedTimestamp = lastUpdatedTimestampStr ? parseInt(lastUpdatedTimestampStr, 10) : null;
85
+ const lastUpdateTimestamp = (!isNaNNumber(lastUpdatedTimestamp) && lastUpdatedTimestamp !== null) ? lastUpdatedTimestamp : null;
86
+
77
87
  if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) {
78
88
  splits.clear();
79
89
  rbSegments.clear();
@@ -90,10 +100,16 @@ export function validateCache(options: SplitIO.InLocalStorageOptions, storage: S
90
100
  // Persist clear
91
101
  if (storage.save) storage.save();
92
102
 
93
- return false;
103
+ return {
104
+ isCacheValid: false,
105
+ lastUpdateTimestamp: null
106
+ };
94
107
  }
95
108
 
96
109
  // Check if ready from cache
97
- return isThereCache;
110
+ return {
111
+ isCacheValid: isThereCache,
112
+ lastUpdateTimestamp
113
+ };
98
114
  });
99
115
  }
@@ -499,7 +499,7 @@ export interface IStorageSync extends IStorageBase<
499
499
  IUniqueKeysCacheSync
500
500
  > {
501
501
  // Defined in client-side
502
- validateCache?: () => Promise<boolean>,
502
+ validateCache?: () => Promise<{ isCacheValid: boolean; lastUpdateTimestamp: number | null }>,
503
503
  largeSegments?: ISegmentsCacheSync,
504
504
  }
505
505
 
@@ -7,9 +7,8 @@ import { syncTaskFactory } from '../../syncTask';
7
7
  import { ISyncTask } from '../../types';
8
8
  import { ISettings } from '../../../types';
9
9
  import { CONTROL } from '../../../utils/constants';
10
- import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants';
10
+ import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED, FLAGS_UPDATE, SEGMENTS_UPDATE } from '../../../readiness/constants';
11
11
  import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants';
12
- import { SdkUpdateMetadataKeys } from '../../polling/types';
13
12
 
14
13
  /**
15
14
  * Offline equivalent of `splitChangesUpdaterFactory`
@@ -56,15 +55,17 @@ export function fromObjectUpdaterFactory(
56
55
  splitsCache.clear(), // required to sync removed splits from mock
57
56
  splitsCache.update(splits, [], Date.now())
58
57
  ]).then(() => {
59
- readiness.splits.emit(SDK_SPLITS_ARRIVED, { type: SdkUpdateMetadataKeys.FLAGS_UPDATE, names: [] });
58
+ readiness.splits.emit(SDK_SPLITS_ARRIVED, { type: FLAGS_UPDATE, names: [] });
60
59
 
61
60
  if (startingUp) {
62
61
  startingUp = false;
63
- Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
62
+ Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then((cacheMetadata) => {
64
63
  // Emits SDK_READY_FROM_CACHE
65
- if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
64
+ if (cacheMetadata.isCacheValid) {
65
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, cacheMetadata);
66
+ }
66
67
  // Emits SDK_READY
67
- readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
68
+ readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
68
69
  });
69
70
  }
70
71
  return true;
@@ -4,20 +4,6 @@ import { IStorageSync } from '../../storages/types';
4
4
  import { MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../streaming/types';
5
5
  import { ITask, ISyncTask } from '../types';
6
6
 
7
- /**
8
- * Metadata keys for SDK update events.
9
- */
10
- export enum SdkUpdateMetadataKeys {
11
- /**
12
- * The update event emitted when the SDK cache is updated with new data for flags.
13
- */
14
- FLAGS_UPDATE = 'FLAGS_UPDATE',
15
- /**
16
- * The update event emitted when the SDK cache is updated with new data for segments.
17
- */
18
- SEGMENTS_UPDATE = 'SEGMENTS_UPDATE'
19
- }
20
-
21
7
  export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }], boolean> { }
22
8
 
23
9
  export interface ISegmentsSyncTask extends ISyncTask<[fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number], boolean> { }
@@ -2,14 +2,13 @@ import { IMySegmentsFetcher } from '../fetchers/types';
2
2
  import { IStorageSync } from '../../../storages/types';
3
3
  import { ISegmentsEventEmitter } from '../../../readiness/types';
4
4
  import { timeout } from '../../../utils/promise/timeout';
5
- import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants';
5
+ import { SDK_SEGMENTS_ARRIVED, SEGMENTS_UPDATE } from '../../../readiness/constants';
6
6
  import { ILogger } from '../../../logger/types';
7
7
  import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants';
8
8
  import { MySegmentsData } from '../types';
9
9
  import { IMembershipsResponse } from '../../../dtos/types';
10
10
  import { MEMBERSHIPS_LS_UPDATE } from '../../streaming/constants';
11
11
  import { usesSegmentsSync } from '../../../storages/AbstractSplitsCacheSync';
12
- import { SdkUpdateMetadataKeys } from '../types';
13
12
 
14
13
  type IMySegmentsUpdater = (segmentsData?: MySegmentsData, noCache?: boolean, till?: number) => Promise<boolean>
15
14
 
@@ -57,7 +56,7 @@ export function mySegmentsUpdaterFactory(
57
56
  // Notify update if required
58
57
  if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
59
58
  readyOnAlreadyExistentState = false;
60
- segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
59
+ segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
61
60
  }
62
61
  }
63
62
 
@@ -1,12 +1,11 @@
1
1
  import { ISegmentChangesFetcher } from '../fetchers/types';
2
2
  import { ISegmentsCacheBase } from '../../../storages/types';
3
3
  import { IReadinessManager } from '../../../readiness/types';
4
- import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants';
4
+ import { SDK_SEGMENTS_ARRIVED, SEGMENTS_UPDATE } from '../../../readiness/constants';
5
5
  import { ILogger } from '../../../logger/types';
6
6
  import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants';
7
7
  import { timeout } from '../../../utils/promise/timeout';
8
8
  import { SdkUpdateMetadata } from '../../../../types/splitio';
9
- import { SdkUpdateMetadataKeys } from '../types';
10
9
 
11
10
 
12
11
  type ISegmentChangesUpdater = (fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number) => Promise<boolean>
@@ -86,11 +85,13 @@ export function segmentChangesUpdaterFactory(
86
85
  // if at least one segment fetch succeeded, mark segments ready
87
86
  if (shouldUpdateFlags.some(update => update) || readyOnAlreadyExistentState) {
88
87
  readyOnAlreadyExistentState = false;
89
- const metadata: SdkUpdateMetadata = {
90
- type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE,
91
- names: []
92
- };
93
- if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);
88
+ if (readiness) {
89
+ const metadata: SdkUpdateMetadata = {
90
+ type: SEGMENTS_UPDATE,
91
+ names: []
92
+ };
93
+ readiness.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);
94
+ }
94
95
  }
95
96
  return true;
96
97
  });
@@ -3,7 +3,7 @@ import { ISplitChangesFetcher } from '../fetchers/types';
3
3
  import { IRBSegment, ISplit, ISplitChangesResponse, ISplitFiltersValidation, MaybeThenable } from '../../../dtos/types';
4
4
  import { ISplitsEventEmitter } from '../../../readiness/types';
5
5
  import { timeout } from '../../../utils/promise/timeout';
6
- import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants';
6
+ import { SDK_SPLITS_ARRIVED, FLAGS_UPDATE, SEGMENTS_UPDATE } from '../../../readiness/constants';
7
7
  import { ILogger } from '../../../logger/types';
8
8
  import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_RBS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants';
9
9
  import { startsWith } from '../../../utils/lang';
@@ -11,7 +11,6 @@ import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT
11
11
  import { setToArray } from '../../../utils/lang/sets';
12
12
  import { SPLIT_UPDATE } from '../../streaming/constants';
13
13
  import { SdkUpdateMetadata } from '../../../../types/splitio';
14
- import { SdkUpdateMetadataKeys } from '../types';
15
14
 
16
15
  export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string };
17
16
  type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise<boolean>
@@ -198,12 +197,14 @@ export function splitChangesUpdaterFactory(
198
197
  return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
199
198
  .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
200
199
  .then(emitSplitsArrivedEvent => {
201
- const metadata: SdkUpdateMetadata = {
202
- type: updatedFlags.length > 0 ? SdkUpdateMetadataKeys.FLAGS_UPDATE : SdkUpdateMetadataKeys.SEGMENTS_UPDATE,
203
- names: updatedFlags.length > 0 ? updatedFlags : []
204
- };
205
200
  // emit SDK events
206
- if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, metadata);
201
+ if (emitSplitsArrivedEvent) {
202
+ const metadata: SdkUpdateMetadata = {
203
+ type: updatedFlags.length > 0 ? FLAGS_UPDATE : SEGMENTS_UPDATE,
204
+ names: updatedFlags.length > 0 ? updatedFlags : []
205
+ };
206
+ splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, metadata);
207
+ }
207
208
  return true;
208
209
  });
209
210
  }
@@ -92,12 +92,14 @@ export function syncManagerOnlineFactory(
92
92
  // @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved
93
93
  submitterManager.start(!isConsentGranted(settings));
94
94
 
95
- return Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
95
+ return Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then((cacheMetadata) => {
96
96
  if (!running) return;
97
97
 
98
98
  if (startFirstTime) {
99
99
  // Emits SDK_READY_FROM_CACHE
100
- if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
100
+ if (cacheMetadata.isCacheValid) {
101
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, cacheMetadata);
102
+ }
101
103
 
102
104
  }
103
105
 
@@ -8,7 +8,7 @@ import { IStorageFactoryParams, IStorageSync } from '../../../storages/types';
8
8
 
9
9
  export function __InLocalStorageMockFactory(params: IStorageFactoryParams): IStorageSync {
10
10
  const result = InMemoryStorageCSFactory(params);
11
- result.validateCache = () => Promise.resolve(true); // to emit SDK_READY_FROM_CACHE
11
+ result.validateCache = () => Promise.resolve({ isCacheValid: true, lastUpdateTimestamp: null }); // to emit SDK_READY_FROM_CACHE
12
12
  return result;
13
13
  }
14
14
  __InLocalStorageMockFactory.type = STORAGE_MEMORY;
@@ -530,7 +530,7 @@ declare namespace SplitIO {
530
530
  /**
531
531
  * Timestamp in milliseconds since epoch when the event was emitted.
532
532
  */
533
- lastUpdateTimestamp: number
533
+ lastUpdateTimestamp: number | null
534
534
  }
535
535
 
536
536
  /**