@splitsoftware/splitio-commons 2.10.2-rc.4 → 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.
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.
@@ -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;
@@ -45,10 +45,11 @@ function fromObjectUpdaterFactory(splitsParser, storage, readiness, settings) {
45
45
  readiness.splits.emit(constants_2.SDK_SPLITS_ARRIVED, { type: constants_2.FLAGS_UPDATE, names: [] });
46
46
  if (startingUp) {
47
47
  startingUp = false;
48
- Promise.resolve(storage.validateCache ? storage.validateCache() : false).then(function (isCacheLoaded) {
48
+ Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then(function (cacheMetadata) {
49
49
  // Emits SDK_READY_FROM_CACHE
50
- if (isCacheLoaded)
51
- 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
+ }
52
53
  // Emits SDK_READY
53
54
  readiness.segments.emit(constants_2.SDK_SEGMENTS_ARRIVED, { type: constants_2.SEGMENTS_UPDATE, names: [] });
54
55
  });
@@ -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;
@@ -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
  }
@@ -42,10 +42,11 @@ export function fromObjectUpdaterFactory(splitsParser, storage, readiness, setti
42
42
  readiness.splits.emit(SDK_SPLITS_ARRIVED, { type: FLAGS_UPDATE, names: [] });
43
43
  if (startingUp) {
44
44
  startingUp = false;
45
- Promise.resolve(storage.validateCache ? storage.validateCache() : false).then(function (isCacheLoaded) {
45
+ Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then(function (cacheMetadata) {
46
46
  // Emits SDK_READY_FROM_CACHE
47
- if (isCacheLoaded)
48
- readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
47
+ if (cacheMetadata.isCacheValid) {
48
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, cacheMetadata);
49
+ }
49
50
  // Emits SDK_READY
50
51
  readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
51
52
  });
@@ -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.4",
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",
@@ -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
 
@@ -59,9 +59,11 @@ export function fromObjectUpdaterFactory(
59
59
 
60
60
  if (startingUp) {
61
61
  startingUp = false;
62
- Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
62
+ Promise.resolve(storage.validateCache ? storage.validateCache() : { isCacheValid: false, lastUpdateTimestamp: null }).then((cacheMetadata) => {
63
63
  // Emits SDK_READY_FROM_CACHE
64
- if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
64
+ if (cacheMetadata.isCacheValid) {
65
+ readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, cacheMetadata);
66
+ }
65
67
  // Emits SDK_READY
66
68
  readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
67
69
  });
@@ -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
  /**