Skip to content

Commit ff585ca

Browse files
Merge pull request #463 from splitio/ready-metadata
sdk_ready metadata
2 parents fe6d284 + 93690bf commit ff585ca

File tree

16 files changed

+109
-79
lines changed

16 files changed

+109
-79
lines changed

CHANGES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
2.11.0 (January XX, 2026)
22
- 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` (milliseconds since epoch).
44

55
2.10.1 (December 18, 2025)
66
- Bugfix - Handle `null` prerequisites properly.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-commons",
3-
"version": "2.10.2-rc.4",
3+
"version": "2.10.2-rc.5",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/readiness/__tests__/readinessManager.spec.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,13 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) =
100100
counter++;
101101
});
102102

103-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
104-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
103+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
104+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
105105
setTimeout(() => {
106-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
106+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
107107
}, 0);
108-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
109-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
110-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
111-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
108+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
109+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
112110

113111
setTimeout(() => {
114112
expect(counter).toBe(1); // should be called only once
@@ -366,19 +364,21 @@ test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', ()
366364
test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when cache is loaded', () => {
367365
const readinessManager = readinessManagerFactory(EventEmitter, settings);
368366

367+
const cacheTimestamp = Date.now() - 1000 * 60 * 60; // 1 hour ago
369368
let receivedMetadata: SdkReadyMetadata | undefined;
370369
readinessManager.gate.on(SDK_READY_FROM_CACHE, (meta: SdkReadyMetadata) => {
371370
receivedMetadata = meta;
372371
});
373372

374-
// Emit cache loaded event
375-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
373+
// Emit cache loaded event with timestamp
374+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, {
375+
initialCacheLoad: false,
376+
lastUpdateTimestamp: cacheTimestamp
377+
});
376378

377379
expect(receivedMetadata).toBeDefined();
378-
expect(receivedMetadata!.initialCacheLoad).toBe(true);
379-
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
380-
// Allow small timing difference (up to 10ms)
381-
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
380+
expect(receivedMetadata!.initialCacheLoad).toBe(false);
381+
expect(receivedMetadata!.lastUpdateTimestamp).toBe(cacheTimestamp);
382382
});
383383

384384
test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when SDK becomes ready without cache', () => {
@@ -394,17 +394,16 @@ test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when SD
394394
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
395395

396396
expect(receivedMetadata).toBeDefined();
397-
expect(receivedMetadata!.initialCacheLoad).toBe(false);
398-
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
399-
// Allow small timing difference (up to 10ms)
400-
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
397+
expect(receivedMetadata!.initialCacheLoad).toBe(true);
398+
expect(receivedMetadata!.lastUpdateTimestamp).toBeUndefined();
401399
});
402400

403401
test('READINESS MANAGER / SDK_READY should emit with metadata when ready from cache', () => {
404402
const readinessManager = readinessManagerFactory(EventEmitter, settings);
405403

406-
// First emit cache loaded
407-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
404+
const cacheTimestamp = Date.now() - 1000 * 60 * 60; // 1 hour ago
405+
// First emit cache loaded with timestamp
406+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: cacheTimestamp });
408407

409408
let receivedMetadata: SdkReadyMetadata | undefined;
410409
readinessManager.gate.on(SDK_READY, (meta: SdkReadyMetadata) => {
@@ -416,10 +415,8 @@ test('READINESS MANAGER / SDK_READY should emit with metadata when ready from ca
416415
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
417416

418417
expect(receivedMetadata).toBeDefined();
419-
expect(receivedMetadata!.initialCacheLoad).toBe(true); // Was ready from cache first
420-
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
421-
// Allow small timing difference (up to 10ms)
422-
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
418+
expect(receivedMetadata!.initialCacheLoad).toBe(false); // Was ready from cache first
419+
expect(receivedMetadata!.lastUpdateTimestamp).toBe(cacheTimestamp);
423420
});
424421

425422
test('READINESS MANAGER / SDK_READY should emit with metadata when ready without cache', () => {
@@ -435,8 +432,6 @@ test('READINESS MANAGER / SDK_READY should emit with metadata when ready without
435432
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
436433

437434
expect(receivedMetadata).toBeDefined();
438-
expect(receivedMetadata!.initialCacheLoad).toBe(false); // Was not ready from cache
439-
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
440-
// Allow small timing difference (up to 10ms)
441-
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
435+
expect(receivedMetadata!.initialCacheLoad).toBe(true); // Was not ready from cache
436+
expect(receivedMetadata!.lastUpdateTimestamp).toBeUndefined(); // No cache timestamp when fresh install
442437
});

src/readiness/__tests__/sdkReadinessManager.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ describe('SDK Readiness Manager - Promises', () => {
219219
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings);
220220

221221
// make the SDK ready from cache
222-
sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
222+
sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: null });
223223
expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(false);
224224

225225
// validate error log for SDK_READY_FROM_CACHE

src/readiness/readinessManager.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { objectAssign } from '../utils/lang/objectAssign';
22
import { ISettings } from '../types';
3-
import SplitIO from '../../types/splitio';
3+
import SplitIO, { SdkReadyMetadata } from '../../types/splitio';
44
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';
55
import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types';
66

@@ -55,6 +55,7 @@ export function readinessManagerFactory(
5555

5656
// emit SDK_READY_FROM_CACHE
5757
let isReadyFromCache = false;
58+
let cacheLastUpdateTimestamp: number | undefined = undefined;
5859
if (splits.splitsCacheLoaded) isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE
5960
else splits.once(SDK_SPLITS_CACHE_LOADED, checkIsReadyFromCache);
6061

@@ -84,17 +85,14 @@ export function readinessManagerFactory(
8485
splits.initCallbacks.push(__init);
8586
if (splits.hasInit) __init();
8687

87-
function checkIsReadyFromCache() {
88+
function checkIsReadyFromCache(cacheMetadata: SdkReadyMetadata) {
89+
cacheLastUpdateTimestamp = cacheMetadata.lastUpdateTimestamp;
8890
isReadyFromCache = true;
8991
// Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
9092
if (!isReady && !isDestroyed) {
9193
try {
9294
syncLastUpdate();
93-
const metadata: SplitIO.SdkReadyMetadata = {
94-
initialCacheLoad: true,
95-
lastUpdateTimestamp: lastUpdate
96-
};
97-
gate.emit(SDK_READY_FROM_CACHE, metadata);
95+
gate.emit(SDK_READY_FROM_CACHE, cacheMetadata);
9896
} catch (e) {
9997
// throws user callback exceptions in next tick
10098
setTimeout(() => { throw e; }, 0);
@@ -121,15 +119,15 @@ export function readinessManagerFactory(
121119
const wasReadyFromCache = isReadyFromCache;
122120
if (!isReadyFromCache) {
123121
isReadyFromCache = true;
124-
const metadataFromCache: SplitIO.SdkReadyMetadata = {
125-
initialCacheLoad: false,
126-
lastUpdateTimestamp: lastUpdate
122+
const metadataReadyFromCache: SplitIO.SdkReadyMetadata = {
123+
initialCacheLoad: true, // Fresh install, no cache existed
124+
lastUpdateTimestamp: undefined // No cache timestamp when fresh install
127125
};
128-
gate.emit(SDK_READY_FROM_CACHE, metadataFromCache);
126+
gate.emit(SDK_READY_FROM_CACHE, metadataReadyFromCache);
129127
}
130128
const metadataReady: SplitIO.SdkReadyMetadata = {
131-
initialCacheLoad: wasReadyFromCache,
132-
lastUpdateTimestamp: lastUpdate
129+
initialCacheLoad: !wasReadyFromCache, // true if not ready from cache (initial load), false if ready from cache
130+
lastUpdateTimestamp: wasReadyFromCache ? cacheLastUpdateTimestamp : undefined
133131
};
134132
gate.emit(SDK_READY, metadataReady);
135133
} catch (e) {

src/sdkFactory/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,18 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
5454
return;
5555
}
5656
readiness.splits.emit(SDK_SPLITS_ARRIVED);
57-
readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
57+
readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { initialCacheLoad: false /* Not an initial load, cache exists */ });
5858
},
5959
onReadyFromCacheCb() {
60-
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
60+
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false /* Not an initial load, cache exists */ });
6161
}
6262
});
6363

6464
const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.fallbackTreatments);
6565

6666
if (initialRolloutPlan) {
6767
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, { initialCacheLoad: false /* Not an initial load, cache exists */ });
6969
}
7070

7171
const clients: Record<string, SplitIO.IBasicClient> = {};

src/storages/inLocalStorage/__tests__/validateCache.spec.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ describe.each(storages)('validateCache', (storage) => {
2929
for (let i = 0; i < storage.length; i++) storage.removeItem(storage.key(i) as string);
3030
});
3131

32-
test('if there is no cache, it should return false', async () => {
33-
expect(await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
32+
test('if there is no cache, it should return initialCacheLoad: true', async () => {
33+
const result = await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments);
34+
expect(result.initialCacheLoad).toBe(true);
35+
expect(result.lastUpdateTimestamp).toBeUndefined();
3436

3537
expect(logSpy).not.toHaveBeenCalled();
3638

@@ -44,12 +46,16 @@ describe.each(storages)('validateCache', (storage) => {
4446
expect(storage.getItem(keys.buildLastClear())).toBeNull();
4547
});
4648

47-
test('if there is cache and it must not be cleared, it should return true', async () => {
49+
test('if there is cache and it must not be cleared, it should return initialCacheLoad: false', async () => {
50+
const lastUpdateTimestamp = Date.now() - 1000 * 60 * 60; // 1 hour ago
4851
storage.setItem(keys.buildSplitsTillKey(), '1');
4952
storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
53+
storage.setItem(keys.buildLastUpdatedKey(), lastUpdateTimestamp + '');
5054
await storage.save && storage.save();
5155

52-
expect(await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);
56+
const result = await validateCache({}, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments);
57+
expect(result.initialCacheLoad).toBe(false);
58+
expect(result.lastUpdateTimestamp).toBe(lastUpdateTimestamp);
5359

5460
expect(logSpy).not.toHaveBeenCalled();
5561

@@ -63,13 +69,15 @@ describe.each(storages)('validateCache', (storage) => {
6369
expect(storage.getItem(keys.buildLastClear())).toBeNull();
6470
});
6571

66-
test('if there is cache and it has expired, it should clear cache and return false', async () => {
72+
test('if there is cache and it has expired, it should clear cache and return initialCacheLoad: true', async () => {
6773
storage.setItem(keys.buildSplitsTillKey(), '1');
6874
storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
6975
storage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago
7076
await storage.save && storage.save();
7177

72-
expect(await validateCache({ expirationDays: 1 }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
78+
const result = await validateCache({ expirationDays: 1 }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments);
79+
expect(result.initialCacheLoad).toBe(true);
80+
expect(result.lastUpdateTimestamp).toBeUndefined();
7381

7482
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache');
7583

@@ -82,12 +90,14 @@ describe.each(storages)('validateCache', (storage) => {
8290
expect(nearlyEqual(parseInt(storage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true);
8391
});
8492

85-
test('if there is cache and its hash has changed, it should clear cache and return false', async () => {
93+
test('if there is cache and its hash has changed, it should clear cache and return initialCacheLoad: true', async () => {
8694
storage.setItem(keys.buildSplitsTillKey(), '1');
8795
storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
8896
await storage.save && storage.save();
8997

90-
expect(await validateCache({}, storage, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
98+
const result = await validateCache({}, storage, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments);
99+
expect(result.initialCacheLoad).toBe(true);
100+
expect(result.lastUpdateTimestamp).toBeUndefined();
91101

92102
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache');
93103

@@ -100,14 +110,16 @@ describe.each(storages)('validateCache', (storage) => {
100110
expect(nearlyEqual(parseInt(storage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true);
101111
});
102112

103-
test('if there is cache and clearOnInit is true, it should clear cache and return false', async () => {
113+
test('if there is cache and clearOnInit is true, it should clear cache and return initialCacheLoad: true', async () => {
104114
// Older cache version (without last clear)
105115
storage.removeItem(keys.buildLastClear());
106116
storage.setItem(keys.buildSplitsTillKey(), '1');
107117
storage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
108118
await storage.save && storage.save();
109119

110-
expect(await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
120+
const result = await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments);
121+
expect(result.initialCacheLoad).toBe(true);
122+
expect(result.lastUpdateTimestamp).toBeUndefined();
111123

112124
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
113125

@@ -122,14 +134,20 @@ describe.each(storages)('validateCache', (storage) => {
122134

123135
// If cache is cleared, it should not clear again until a day has passed
124136
logSpy.mockClear();
137+
const lastUpdateTimestamp = Date.now() - 1000 * 60 * 60; // 1 hour ago
125138
storage.setItem(keys.buildSplitsTillKey(), '1');
126-
expect(await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);
139+
storage.setItem(keys.buildLastUpdatedKey(), lastUpdateTimestamp + '');
140+
const result2 = await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments);
141+
expect(result2.initialCacheLoad).toBe(false);
142+
expect(result2.lastUpdateTimestamp).toBe(lastUpdateTimestamp);
127143
expect(logSpy).not.toHaveBeenCalled();
128144
expect(storage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed
129145

130146
// If a day has passed, it should clear again
131147
storage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + '');
132-
expect(await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
148+
const result3 = await validateCache({ clearOnInit: true }, storage, fullSettings, keys, splits, rbSegments, segments, largeSegments);
149+
expect(result3.initialCacheLoad).toBe(true);
150+
expect(result3.lastUpdateTimestamp).toBeUndefined();
133151
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
134152
expect(splits.clear).toHaveBeenCalledTimes(2);
135153
expect(rbSegments.clear).toHaveBeenCalledTimes(2);

src/storages/inLocalStorage/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
5454
const rbSegments = new RBSegmentsCacheInLocal(settings, keys, storage);
5555
const segments = new MySegmentsCacheInLocal(log, keys, storage);
5656
const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage);
57-
let validateCachePromise: Promise<boolean> | undefined;
57+
let validateCachePromise: Promise<SplitIO.SdkReadyMetadata> | undefined;
5858

5959
return {
6060
splits,
@@ -68,7 +68,10 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
6868
uniqueKeys: new UniqueKeysCacheInMemoryCS(),
6969

7070
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;
7275
},
7376

7477
save() {

0 commit comments

Comments
 (0)