Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: `true` for fresh install/first app launch, `false` for warm cache/second app launch) and `lastUpdateTimestamp` (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.6",
"description": "Split JavaScript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
151 changes: 143 additions & 8 deletions 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 @@ -99,15 +100,13 @@ test('READINESS MANAGER / Ready from cache event should be fired once', (done) =
counter++;
});

readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
setTimeout(() => {
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
}, 0);
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false, lastUpdateTimestamp: undefined });

setTimeout(() => {
expect(counter).toBe(1); // should be called only once
Expand Down Expand Up @@ -300,3 +299,139 @@ 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);

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

// Emit cache loaded event with timestamp
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED, {
initialCacheLoad: false,
lastUpdateTimestamp: cacheTimestamp
});

expect(receivedMetadata).toBeDefined();
expect(receivedMetadata!.initialCacheLoad).toBe(false);
expect(receivedMetadata!.lastUpdateTimestamp).toBe(cacheTimestamp);
});

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(true);
expect(receivedMetadata!.lastUpdateTimestamp).toBeUndefined();
});

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

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

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(false); // Was ready from cache first
expect(receivedMetadata!.lastUpdateTimestamp).toBe(cacheTimestamp);
});

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(true); // Was not ready from cache
expect(receivedMetadata!.lastUpdateTimestamp).toBeUndefined(); // No cache timestamp when fresh install
});
2 changes: 1 addition & 1 deletion src/readiness/__tests__/sdkReadinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ describe('SDK Readiness Manager - Promises', () => {
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings);

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

// validate error log for SDK_READY_FROM_CACHE
Expand Down
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: 17 additions & 8 deletions src/readiness/readinessManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { objectAssign } from '../utils/lang/objectAssign';
import { ISettings } from '../types';
import SplitIO from '../../types/splitio';
import SplitIO, { SdkReadyMetadata } from '../../types/splitio';
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';
import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types';

Expand All @@ -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 @@ -53,6 +53,10 @@ export function readinessManagerFactory(
lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1;
}

let metadataReady: SdkReadyMetadata = {
initialCacheLoad: true
};

// emit SDK_READY_FROM_CACHE
let isReadyFromCache = false;
if (splits.splitsCacheLoaded) isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE
Expand Down Expand Up @@ -84,26 +88,27 @@ export function readinessManagerFactory(
splits.initCallbacks.push(__init);
if (splits.hasInit) __init();

function checkIsReadyFromCache() {
function checkIsReadyFromCache(cacheMetadata: SdkReadyMetadata) {
metadataReady = cacheMetadata;
isReadyFromCache = true;
// Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
if (!isReady && !isDestroyed) {
try {
syncLastUpdate();
gate.emit(SDK_READY_FROM_CACHE, isReady);
gate.emit(SDK_READY_FROM_CACHE, cacheMetadata);
} 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 @@ -116,9 +121,13 @@ export function readinessManagerFactory(
syncLastUpdate();
if (!isReadyFromCache) {
isReadyFromCache = true;
gate.emit(SDK_READY_FROM_CACHE, isReady);
const metadataReadyFromCache: SplitIO.SdkReadyMetadata = {
initialCacheLoad: true, // Fresh install, no cache existed
lastUpdateTimestamp: undefined // No cache timestamp when fresh install
};
gate.emit(SDK_READY_FROM_CACHE, metadataReadyFromCache);
}
gate.emit(SDK_READY);
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
2 changes: 1 addition & 1 deletion src/sdkFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA

if (initialRolloutPlan) {
setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key));
if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED, { initialCacheLoad: false /* Not an initial load, cache exists */ });
}

const clients: Record<string, SplitIO.IBasicClient> = {};
Expand Down
Loading