Skip to content

Commit 975e456

Browse files
Merge pull request #461 from splitio/fme-12059
[FME-12059] SDK_UPDATE with metadata
2 parents dbdb182 + e57a2c6 commit 975e456

28 files changed

+816
-166
lines changed

.eslintrc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,14 @@
9494
{
9595
"files": ["types/**"],
9696
"rules": {
97-
"no-use-before-define": "off"
97+
"no-use-before-define": "off",
98+
"@typescript-eslint/member-delimiter-style": [
99+
"error",
100+
{
101+
"multiline": { "delimiter": "semi", "requireLast": true },
102+
"singleline": { "delimiter": "semi", "requireLast": true }
103+
}
104+
]
98105
}
99106
},
100107
{

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2.11.0 (January XX, 2026)
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: `true` for fresh install/first app launch, `false` for warm cache/second app launch) and `lastUpdateTimestamp` (milliseconds since epoch).
4+
15
2.10.1 (December 18, 2025)
26
- Bugfix - Handle `null` prerequisites properly.
37

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.1",
3+
"version": "2.10.2-rc.6",
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: 143 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { readinessManagerFactory } from '../readinessManager';
22
import { EventEmitter } from '../../utils/MinEvents';
33
import { IReadinessManager } from '../types';
4-
import { SDK_READY, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_READY_FROM_CACHE, SDK_SPLITS_CACHE_LOADED, SDK_READY_TIMED_OUT } from '../constants';
4+
import { SDK_READY, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_READY_FROM_CACHE, SDK_SPLITS_CACHE_LOADED, SDK_READY_TIMED_OUT, FLAGS_UPDATE, SEGMENTS_UPDATE } from '../constants';
55
import { ISettings } from '../../types';
6+
import { SdkUpdateMetadata, SdkReadyMetadata } from '../../../types/splitio';
67

78
const settings = {
89
startup: {
@@ -99,15 +100,13 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) =
99100
counter++;
100101
});
101102

102-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
103-
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 });
104105
setTimeout(() => {
105-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
106+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
106107
}, 0);
107-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
108-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
109-
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
110-
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 });
111110

112111
setTimeout(() => {
113112
expect(counter).toBe(1); // should be called only once
@@ -300,3 +299,139 @@ test('READINESS MANAGER / Destroy before it was ready and timedout', (done) => {
300299
}, settingsWithTimeout.startup.readyTimeout * 1.5);
301300

302301
});
302+
303+
test('READINESS MANAGER / SDK_UPDATE should emit with metadata', () => {
304+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
305+
306+
// SDK_READY
307+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
308+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
309+
310+
const metadata: SdkUpdateMetadata = {
311+
type: FLAGS_UPDATE,
312+
names: ['flag1', 'flag2']
313+
};
314+
315+
let receivedMetadata: SdkUpdateMetadata | undefined;
316+
readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => {
317+
receivedMetadata = meta;
318+
});
319+
320+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED, metadata);
321+
322+
expect(receivedMetadata).toEqual(metadata);
323+
});
324+
325+
test('READINESS MANAGER / SDK_UPDATE should handle undefined metadata', () => {
326+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
327+
328+
// SDK_READY
329+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
330+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
331+
332+
let receivedMetadata: any;
333+
readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => {
334+
receivedMetadata = meta;
335+
});
336+
337+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
338+
339+
expect(receivedMetadata).toBeUndefined();
340+
});
341+
342+
test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', () => {
343+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
344+
345+
// SDK_READY
346+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
347+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
348+
349+
const metadata: SdkUpdateMetadata = {
350+
type: SEGMENTS_UPDATE,
351+
names: []
352+
};
353+
354+
let receivedMetadata: SdkUpdateMetadata | undefined;
355+
readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => {
356+
receivedMetadata = meta;
357+
});
358+
359+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);
360+
361+
expect(receivedMetadata).toEqual(metadata);
362+
});
363+
364+
test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when cache is loaded', () => {
365+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
366+
367+
const cacheTimestamp = Date.now() - 1000 * 60 * 60; // 1 hour ago
368+
let receivedMetadata: SdkReadyMetadata | undefined;
369+
readinessManager.gate.on(SDK_READY_FROM_CACHE, (meta: SdkReadyMetadata) => {
370+
receivedMetadata = meta;
371+
});
372+
373+
// Emit cache loaded event with timestamp
374+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, {
375+
initialCacheLoad: false,
376+
lastUpdateTimestamp: cacheTimestamp
377+
});
378+
379+
expect(receivedMetadata).toBeDefined();
380+
expect(receivedMetadata!.initialCacheLoad).toBe(false);
381+
expect(receivedMetadata!.lastUpdateTimestamp).toBe(cacheTimestamp);
382+
});
383+
384+
test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when SDK becomes ready without cache', () => {
385+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
386+
387+
let receivedMetadata: SdkReadyMetadata | undefined;
388+
readinessManager.gate.on(SDK_READY_FROM_CACHE, (meta: SdkReadyMetadata) => {
389+
receivedMetadata = meta;
390+
});
391+
392+
// Make SDK ready without cache first
393+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
394+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
395+
396+
expect(receivedMetadata).toBeDefined();
397+
expect(receivedMetadata!.initialCacheLoad).toBe(true);
398+
expect(receivedMetadata!.lastUpdateTimestamp).toBeUndefined();
399+
});
400+
401+
test('READINESS MANAGER / SDK_READY should emit with metadata when ready from cache', () => {
402+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
403+
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 });
407+
408+
let receivedMetadata: SdkReadyMetadata | undefined;
409+
readinessManager.gate.on(SDK_READY, (meta: SdkReadyMetadata) => {
410+
receivedMetadata = meta;
411+
});
412+
413+
// Make SDK ready
414+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
415+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
416+
417+
expect(receivedMetadata).toBeDefined();
418+
expect(receivedMetadata!.initialCacheLoad).toBe(false); // Was ready from cache first
419+
expect(receivedMetadata!.lastUpdateTimestamp).toBe(cacheTimestamp);
420+
});
421+
422+
test('READINESS MANAGER / SDK_READY should emit with metadata when ready without cache', () => {
423+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
424+
425+
let receivedMetadata: SdkReadyMetadata | undefined;
426+
readinessManager.gate.on(SDK_READY, (meta: SdkReadyMetadata) => {
427+
receivedMetadata = meta;
428+
});
429+
430+
// Make SDK ready without cache
431+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
432+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
433+
434+
expect(receivedMetadata).toBeDefined();
435+
expect(receivedMetadata!.initialCacheLoad).toBe(true); // Was not ready from cache
436+
expect(receivedMetadata!.lastUpdateTimestamp).toBeUndefined(); // No cache timestamp when fresh install
437+
});

src/readiness/__tests__/sdkReadinessManager.spec.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -219,20 +219,21 @@ 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);
223-
expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(false);
222+
sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: null });
223+
expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toEqual({ initialCacheLoad: false, lastUpdateTimestamp: null });
224224

225225
// validate error log for SDK_READY_FROM_CACHE
226226
expect(loggerMock.error).not.toBeCalled();
227-
sdkReadinessManager.readinessManager.gate.on(SDK_READY_FROM_CACHE, () => {});
227+
sdkReadinessManager.readinessManager.gate.on(SDK_READY_FROM_CACHE, () => { });
228228
expect(loggerMock.error).toBeCalledWith(ERROR_CLIENT_LISTENER, ['SDK_READY_FROM_CACHE']);
229229

230230
const readyFromCache = sdkReadinessManager.sdkStatus.whenReadyFromCache();
231231
const ready = sdkReadinessManager.sdkStatus.whenReady();
232232

233233
// make the SDK ready
234234
emitReadyEvent(sdkReadinessManager.readinessManager);
235-
expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(true);
235+
expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toEqual({ initialCacheLoad: false, lastUpdateTimestamp: null });
236+
expect(await sdkReadinessManager.sdkStatus.whenReady()).toEqual({ initialCacheLoad: false, lastUpdateTimestamp: null });
236237

237238
let testPassedCount = 0;
238239
function incTestPassedCount() { testPassedCount++; }
@@ -260,12 +261,12 @@ describe('SDK Readiness Manager - Promises', () => {
260261
function incTestPassedCount() { testPassedCount++; }
261262
function throwTestFailed() { throw new Error('It should rejected, not resolved.'); }
262263

263-
await readyFromCacheForTimeout.then(throwTestFailed,incTestPassedCount);
264-
await readyForTimeout.then(throwTestFailed,incTestPassedCount);
264+
await readyFromCacheForTimeout.then(throwTestFailed, incTestPassedCount);
265+
await readyForTimeout.then(throwTestFailed, incTestPassedCount);
265266

266267
// any subsequent call to .whenReady() and .whenReadyFromCache() must be a rejected promise until the SDK is ready
267-
await sdkReadinessManagerForTimedout.sdkStatus.whenReadyFromCache().then(throwTestFailed,incTestPassedCount);
268-
await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then(throwTestFailed,incTestPassedCount);
268+
await sdkReadinessManagerForTimedout.sdkStatus.whenReadyFromCache().then(throwTestFailed, incTestPassedCount);
269+
await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then(throwTestFailed, incTestPassedCount);
269270

270271
// make the SDK ready
271272
emitReadyEvent(sdkReadinessManagerForTimedout.readinessManager);

src/readiness/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ export const SDK_READY_TIMED_OUT = 'init::timeout';
1010
export const SDK_READY = 'init::ready';
1111
export const SDK_READY_FROM_CACHE = 'init::cache-ready';
1212
export const SDK_UPDATE = 'state::update';
13+
14+
// SdkUpdateMetadata types:
15+
export const FLAGS_UPDATE = 'FLAGS_UPDATE';
16+
export const SEGMENTS_UPDATE = 'SEGMENTS_UPDATE';

src/readiness/readinessManager.ts

Lines changed: 19 additions & 9 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

@@ -15,7 +15,7 @@ function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter
1515
// `isSplitKill` condition avoids an edge-case of wrongly emitting SDK_READY if:
1616
// - `/memberships` fetch and SPLIT_KILL occurs before `/splitChanges` fetch, and
1717
// - storage has cached splits (for which case `splitsStorage.killLocally` can return true)
18-
splitsEventEmitter.on(SDK_SPLITS_ARRIVED, (isSplitKill: boolean) => { if (!isSplitKill) splitsEventEmitter.splitsArrived = true; });
18+
splitsEventEmitter.on(SDK_SPLITS_ARRIVED, (metadata: SplitIO.SdkUpdateMetadata, isSplitKill: boolean) => { if (!isSplitKill) splitsEventEmitter.splitsArrived = true; });
1919
splitsEventEmitter.once(SDK_SPLITS_CACHE_LOADED, () => { splitsEventEmitter.splitsCacheLoaded = true; });
2020

2121
return splitsEventEmitter;
@@ -53,6 +53,10 @@ export function readinessManagerFactory(
5353
lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1;
5454
}
5555

56+
let metadataReady: SdkReadyMetadata = {
57+
initialCacheLoad: true
58+
};
59+
5660
// emit SDK_READY_FROM_CACHE
5761
let isReadyFromCache = false;
5862
if (splits.splitsCacheLoaded) isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE
@@ -84,26 +88,27 @@ export function readinessManagerFactory(
8488
splits.initCallbacks.push(__init);
8589
if (splits.hasInit) __init();
8690

87-
function checkIsReadyFromCache() {
91+
function checkIsReadyFromCache(cacheMetadata: SdkReadyMetadata) {
92+
metadataReady = cacheMetadata;
8893
isReadyFromCache = true;
8994
// Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
9095
if (!isReady && !isDestroyed) {
9196
try {
9297
syncLastUpdate();
93-
gate.emit(SDK_READY_FROM_CACHE, isReady);
98+
gate.emit(SDK_READY_FROM_CACHE, cacheMetadata);
9499
} catch (e) {
95100
// throws user callback exceptions in next tick
96101
setTimeout(() => { throw e; }, 0);
97102
}
98103
}
99104
}
100105

101-
function checkIsReadyOrUpdate(diff: any) {
106+
function checkIsReadyOrUpdate(metadata: SplitIO.SdkUpdateMetadata) {
102107
if (isDestroyed) return;
103108
if (isReady) {
104109
try {
105110
syncLastUpdate();
106-
gate.emit(SDK_UPDATE, diff);
111+
gate.emit(SDK_UPDATE, metadata);
107112
} catch (e) {
108113
// throws user callback exceptions in next tick
109114
setTimeout(() => { throw e; }, 0);
@@ -116,9 +121,13 @@ export function readinessManagerFactory(
116121
syncLastUpdate();
117122
if (!isReadyFromCache) {
118123
isReadyFromCache = true;
119-
gate.emit(SDK_READY_FROM_CACHE, isReady);
124+
const metadataReadyFromCache: SplitIO.SdkReadyMetadata = {
125+
initialCacheLoad: true, // Fresh install, no cache existed
126+
lastUpdateTimestamp: undefined // No cache timestamp when fresh install
127+
};
128+
gate.emit(SDK_READY_FROM_CACHE, metadataReadyFromCache);
120129
}
121-
gate.emit(SDK_READY);
130+
gate.emit(SDK_READY, metadataReady);
122131
} catch (e) {
123132
// throws user callback exceptions in next tick
124133
setTimeout(() => { throw e; }, 0);
@@ -163,7 +172,8 @@ export function readinessManagerFactory(
163172
hasTimedout() { return hasTimedout; },
164173
isDestroyed() { return isDestroyed; },
165174
isOperational() { return (isReady || isReadyFromCache) && !isDestroyed; },
166-
lastUpdate() { return lastUpdate; }
175+
lastUpdate() { return lastUpdate; },
176+
metadataReady() { return metadataReady; }
167177
};
168178

169179
}

src/readiness/sdkReadinessManager.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ export function sdkReadinessManagerFactory(
120120
},
121121

122122
whenReady() {
123-
return new Promise<void>((resolve, reject) => {
123+
return new Promise<SplitIO.SdkReadyMetadata>((resolve, reject) => {
124124
if (readinessManager.isReady()) {
125-
resolve();
125+
resolve(readinessManager.metadataReady());
126126
} else if (readinessManager.hasTimedout()) {
127127
reject(TIMEOUT_ERROR);
128128
} else {
@@ -133,13 +133,13 @@ export function sdkReadinessManagerFactory(
133133
},
134134

135135
whenReadyFromCache() {
136-
return new Promise<boolean>((resolve, reject) => {
136+
return new Promise<SplitIO.SdkReadyMetadata>((resolve, reject) => {
137137
if (readinessManager.isReadyFromCache()) {
138-
resolve(readinessManager.isReady());
138+
resolve(readinessManager.metadataReady());
139139
} else if (readinessManager.hasTimedout()) {
140140
reject(TIMEOUT_ERROR);
141141
} else {
142-
readinessManager.gate.once(SDK_READY_FROM_CACHE, () => resolve(readinessManager.isReady()));
142+
readinessManager.gate.once(SDK_READY_FROM_CACHE, resolve);
143143
readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR));
144144
}
145145
});

0 commit comments

Comments
 (0)