Skip to content
Draft
4 changes: 4 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
2.11.0 (January XX, 2026)
- 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.
- 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).

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

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-commons",
"version": "2.10.1",
"version": "2.10.2-rc.4",
"description": "Split JavaScript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
142 changes: 141 additions & 1 deletion src/readiness/__tests__/readinessManager.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { readinessManagerFactory } from '../readinessManager';
import { EventEmitter } from '../../utils/MinEvents';
import { IReadinessManager } from '../types';
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';
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';
import { ISettings } from '../../types';
import { SdkUpdateMetadata, SdkReadyMetadata } from '../../../types/splitio';

const settings = {
startup: {
Expand Down Expand Up @@ -300,3 +301,142 @@ test('READINESS MANAGER / Destroy before it was ready and timedout', (done) => {
}, settingsWithTimeout.startup.readyTimeout * 1.5);

});

test('READINESS MANAGER / SDK_UPDATE should emit with metadata', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

// SDK_READY
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

const metadata: SdkUpdateMetadata = {
type: FLAGS_UPDATE,
names: ['flag1', 'flag2']
};

let receivedMetadata: SdkUpdateMetadata | undefined;
readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => {
receivedMetadata = meta;
});

readinessManager.splits.emit(SDK_SPLITS_ARRIVED, metadata);

expect(receivedMetadata).toEqual(metadata);
});

test('READINESS MANAGER / SDK_UPDATE should handle undefined metadata', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

// SDK_READY
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

let receivedMetadata: any;
readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => {
receivedMetadata = meta;
});

readinessManager.splits.emit(SDK_SPLITS_ARRIVED);

expect(receivedMetadata).toBeUndefined();
});

test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

// SDK_READY
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

const metadata: SdkUpdateMetadata = {
type: SEGMENTS_UPDATE,
names: []
};

let receivedMetadata: SdkUpdateMetadata | undefined;
readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => {
receivedMetadata = meta;
});

readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);

expect(receivedMetadata).toEqual(metadata);
});

test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when cache is loaded', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

let receivedMetadata: SdkReadyMetadata | undefined;
readinessManager.gate.on(SDK_READY_FROM_CACHE, (meta: SdkReadyMetadata) => {
receivedMetadata = meta;
});

// Emit cache loaded event
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);

expect(receivedMetadata).toBeDefined();
expect(receivedMetadata!.initialCacheLoad).toBe(true);
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
// Allow small timing difference (up to 10ms)
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
});

test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when SDK becomes ready without cache', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

let receivedMetadata: SdkReadyMetadata | undefined;
readinessManager.gate.on(SDK_READY_FROM_CACHE, (meta: SdkReadyMetadata) => {
receivedMetadata = meta;
});

// Make SDK ready without cache first
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

expect(receivedMetadata).toBeDefined();
expect(receivedMetadata!.initialCacheLoad).toBe(false);
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
// Allow small timing difference (up to 10ms)
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
});

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

// First emit cache loaded
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);

let receivedMetadata: SdkReadyMetadata | undefined;
readinessManager.gate.on(SDK_READY, (meta: SdkReadyMetadata) => {
receivedMetadata = meta;
});

// Make SDK ready
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

expect(receivedMetadata).toBeDefined();
expect(receivedMetadata!.initialCacheLoad).toBe(true); // Was ready from cache first
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
// Allow small timing difference (up to 10ms)
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
});

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

let receivedMetadata: SdkReadyMetadata | undefined;
readinessManager.gate.on(SDK_READY, (meta: SdkReadyMetadata) => {
receivedMetadata = meta;
});

// Make SDK ready without cache
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

expect(receivedMetadata).toBeDefined();
expect(receivedMetadata!.initialCacheLoad).toBe(false); // Was not ready from cache
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
// Allow small timing difference (up to 10ms)
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
});
4 changes: 4 additions & 0 deletions src/readiness/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export const SDK_READY_TIMED_OUT = 'init::timeout';
export const SDK_READY = 'init::ready';
export const SDK_READY_FROM_CACHE = 'init::cache-ready';
export const SDK_UPDATE = 'state::update';

// SdkUpdateMetadata types:
export const FLAGS_UPDATE = 'FLAGS_UPDATE';
export const SEGMENTS_UPDATE = 'SEGMENTS_UPDATE';
25 changes: 19 additions & 6 deletions src/readiness/readinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter
// `isSplitKill` condition avoids an edge-case of wrongly emitting SDK_READY if:
// - `/memberships` fetch and SPLIT_KILL occurs before `/splitChanges` fetch, and
// - storage has cached splits (for which case `splitsStorage.killLocally` can return true)
splitsEventEmitter.on(SDK_SPLITS_ARRIVED, (isSplitKill: boolean) => { if (!isSplitKill) splitsEventEmitter.splitsArrived = true; });
splitsEventEmitter.on(SDK_SPLITS_ARRIVED, (metadata: SplitIO.SdkUpdateMetadata, isSplitKill: boolean) => { if (!isSplitKill) splitsEventEmitter.splitsArrived = true; });
splitsEventEmitter.once(SDK_SPLITS_CACHE_LOADED, () => { splitsEventEmitter.splitsCacheLoaded = true; });

return splitsEventEmitter;
Expand Down Expand Up @@ -90,20 +90,24 @@ export function readinessManagerFactory(
if (!isReady && !isDestroyed) {
try {
syncLastUpdate();
gate.emit(SDK_READY_FROM_CACHE, isReady);
const metadata: SplitIO.SdkReadyMetadata = {
initialCacheLoad: true,
lastUpdateTimestamp: lastUpdate
};
gate.emit(SDK_READY_FROM_CACHE, metadata);
} catch (e) {
// throws user callback exceptions in next tick
setTimeout(() => { throw e; }, 0);
}
}
}

function checkIsReadyOrUpdate(diff: any) {
function checkIsReadyOrUpdate(metadata: SplitIO.SdkUpdateMetadata) {
if (isDestroyed) return;
if (isReady) {
try {
syncLastUpdate();
gate.emit(SDK_UPDATE, diff);
gate.emit(SDK_UPDATE, metadata);
} catch (e) {
// throws user callback exceptions in next tick
setTimeout(() => { throw e; }, 0);
Expand All @@ -114,11 +118,20 @@ export function readinessManagerFactory(
isReady = true;
try {
syncLastUpdate();
const wasReadyFromCache = isReadyFromCache;
if (!isReadyFromCache) {
isReadyFromCache = true;
gate.emit(SDK_READY_FROM_CACHE, isReady);
const metadataFromCache: SplitIO.SdkReadyMetadata = {
initialCacheLoad: false,
lastUpdateTimestamp: lastUpdate
};
gate.emit(SDK_READY_FROM_CACHE, metadataFromCache);
}
gate.emit(SDK_READY);
const metadataReady: SplitIO.SdkReadyMetadata = {
initialCacheLoad: wasReadyFromCache,
lastUpdateTimestamp: lastUpdate
};
gate.emit(SDK_READY, metadataReady);
} catch (e) {
// throws user callback exceptions in next tick
setTimeout(() => { throw e; }, 0);
Expand Down
40 changes: 27 additions & 13 deletions src/readiness/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
import SplitIO from '../../types/splitio';

/** Readiness event types */

export type SDK_READY_TIMED_OUT = 'init::timeout'
export type SDK_READY = 'init::ready'
export type SDK_READY_FROM_CACHE = 'init::cache-ready'
export type SDK_UPDATE = 'state::update'
export type SDK_DESTROY = 'state::destroy'

export type IReadinessEvent = SDK_READY_TIMED_OUT | SDK_READY | SDK_READY_FROM_CACHE | SDK_UPDATE | SDK_DESTROY

export interface IReadinessEventEmitter extends SplitIO.IEventEmitter {
emit(event: IReadinessEvent, ...args: any[]): boolean
on(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
on(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
on(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this;
once(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
once(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
once(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
once(event: string | symbol, listener: (...args: any[]) => void): this;
addListener(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
addListener(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
addListener(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
addListener(event: string | symbol, listener: (...args: any[]) => void): this;
}
/** Splits data emitter */

type SDK_SPLITS_ARRIVED = 'state::splits-arrived'
Expand All @@ -9,6 +34,7 @@ type ISplitsEvent = SDK_SPLITS_ARRIVED | SDK_SPLITS_CACHE_LOADED
export interface ISplitsEventEmitter extends SplitIO.IEventEmitter {
emit(event: ISplitsEvent, ...args: any[]): boolean
on(event: ISplitsEvent, listener: (...args: any[]) => void): this;
on(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
once(event: ISplitsEvent, listener: (...args: any[]) => void): this;
splitsArrived: boolean
splitsCacheLoaded: boolean
Expand All @@ -24,23 +50,11 @@ type ISegmentsEvent = SDK_SEGMENTS_ARRIVED
export interface ISegmentsEventEmitter extends SplitIO.IEventEmitter {
emit(event: ISegmentsEvent, ...args: any[]): boolean
on(event: ISegmentsEvent, listener: (...args: any[]) => void): this;
on(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
once(event: ISegmentsEvent, listener: (...args: any[]) => void): this;
segmentsArrived: boolean
}

/** Readiness emitter */

export type SDK_READY_TIMED_OUT = 'init::timeout'
export type SDK_READY = 'init::ready'
export type SDK_READY_FROM_CACHE = 'init::cache-ready'
export type SDK_UPDATE = 'state::update'
export type SDK_DESTROY = 'state::destroy'
export type IReadinessEvent = SDK_READY_TIMED_OUT | SDK_READY | SDK_READY_FROM_CACHE | SDK_UPDATE | SDK_DESTROY

export interface IReadinessEventEmitter extends SplitIO.IEventEmitter {
emit(event: IReadinessEvent, ...args: any[]): boolean
}

/** Readiness manager */

export interface IReadinessManager {
Expand Down
Loading