Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a719bb7
[FME-12059] SDK_UPDATE with metadata
ZamoraEmmanuel Jan 5, 2026
ef651cc
Fix types
ZamoraEmmanuel Jan 6, 2026
f4a6988
Update metadata properties
ZamoraEmmanuel Jan 12, 2026
631366d
Move enum and type to namespace
ZamoraEmmanuel Jan 12, 2026
6f054c9
Remove names when segments update
ZamoraEmmanuel Jan 12, 2026
c9795ea
avoid metadata when rbs update
ZamoraEmmanuel Jan 13, 2026
cb807b5
Merge branch 'development' of github.com:splitio/javascript-commons i…
ZamoraEmmanuel Jan 15, 2026
27dc057
Add ready metadata and polling
ZamoraEmmanuel Jan 17, 2026
22ef058
Fix ts and add changes file
ZamoraEmmanuel Jan 18, 2026
c60ab9e
Remove enum from namespace
ZamoraEmmanuel Jan 18, 2026
ffe940a
Add SdkUpdateMetadataKeys type
ZamoraEmmanuel Jan 18, 2026
fe6d284
remove enum
ZamoraEmmanuel Jan 19, 2026
5464fbc
sdk_ready metadata
ZamoraEmmanuel Jan 21, 2026
dc47929
Fix test
ZamoraEmmanuel Jan 21, 2026
e388a13
Use initalCacheLad instead of isCacheValid
ZamoraEmmanuel Jan 22, 2026
93690bf
Merge pull request #465 from splitio/initial-cache
ZamoraEmmanuel Jan 22, 2026
ff585ca
Merge pull request #463 from splitio/ready-metadata
ZamoraEmmanuel Jan 22, 2026
6c46d7c
Merge branch 'development' into fme-12059
ZamoraEmmanuel Jan 22, 2026
fc78250
Simplify SDK_READY metadata handling by removing redundant logic
EmilianoSanchez Jan 22, 2026
1fcebd4
Polishing
EmilianoSanchez Jan 22, 2026
25fe09a
Fix message
ZamoraEmmanuel Jan 22, 2026
2ce34d1
Enforce semicolon delimiter style in TypeScript type definitions
EmilianoSanchez Jan 23, 2026
8674fdf
Update whenReady and whenReadyFromCache to return SdkReadyMetadata ob…
EmilianoSanchez Jan 23, 2026
e57a2c6
Merge pull request #466 from splitio/fme-12059-promises
EmilianoSanchez Jan 23, 2026
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 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.1",
"description": "Split JavaScript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
87 changes: 84 additions & 3 deletions src/readiness/__tests__/readinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ 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 { ISettings } from '../../types';
import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../../sync/polling/types';
import { SdkUpdateMetadata, SdkReadyMetadata } from '../../../types/splitio';
import { SdkUpdateMetadataKeys } from '../../sync/polling/types';

const settings = {
startup: {
Expand Down Expand Up @@ -310,7 +311,8 @@ test('READINESS MANAGER / SDK_UPDATE should emit with metadata', () => {
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

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

let receivedMetadata: SdkUpdateMetadata | undefined;
Expand Down Expand Up @@ -348,7 +350,8 @@ test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', ()
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

const metadata: SdkUpdateMetadata = {
[SdkUpdateMetadataKeys.UPDATED_SEGMENTS]: ['segment1', 'segment2']
type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE,
names: []
};

let receivedMetadata: SdkUpdateMetadata | undefined;
Expand All @@ -360,3 +363,81 @@ test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', ()

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);
});
24 changes: 18 additions & 6 deletions src/readiness/readinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ISettings } from '../types';
import SplitIO 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';
import { SdkUpdateMetadata } from '../sync/polling/types';

function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter {
const splitsEventEmitter = objectAssign(new EventEmitter(), {
Expand All @@ -16,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, (metadata: SdkUpdateMetadata, 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 @@ -91,15 +90,19 @@ 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(metadata: SdkUpdateMetadata) {
function checkIsReadyOrUpdate(metadata: SplitIO.SdkUpdateMetadata) {
if (isDestroyed) return;
if (isReady) {
try {
Expand All @@ -115,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
17 changes: 14 additions & 3 deletions src/readiness/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import SplitIO from '../../types/splitio';
import { SdkUpdateMetadata } from '../sync/polling/types';

/** Readiness event types */

Expand All @@ -13,6 +12,18 @@ export type IReadinessEvent = SDK_READY_TIMED_OUT | SDK_READY | SDK_READY_FROM_C

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 */

Expand All @@ -23,7 +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: SdkUpdateMetadata) => 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 @@ -39,7 +50,7 @@ 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: SdkUpdateMetadata) => void): this;
on(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
once(event: ISegmentsEvent, listener: (...args: any[]) => void): this;
segmentsArrived: boolean
}
Expand Down
5 changes: 3 additions & 2 deletions src/sync/offline/syncTasks/fromObjectSyncTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ISettings } from '../../../types';
import { CONTROL } from '../../../utils/constants';
import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants';
import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants';
import { SdkUpdateMetadataKeys } from '../../polling/types';

/**
* Offline equivalent of `splitChangesUpdaterFactory`
Expand Down Expand Up @@ -55,15 +56,15 @@ export function fromObjectUpdaterFactory(
splitsCache.clear(), // required to sync removed splits from mock
splitsCache.update(splits, [], Date.now())
]).then(() => {
readiness.splits.emit(SDK_SPLITS_ARRIVED);
readiness.splits.emit(SDK_SPLITS_ARRIVED, { type: SdkUpdateMetadataKeys.FLAGS_UPDATE, names: [] });

if (startingUp) {
startingUp = false;
Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
// Emits SDK_READY_FROM_CACHE
if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
// Emits SDK_READY
readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
});
}
return true;
Expand Down
5 changes: 3 additions & 2 deletions src/sync/polling/pollingManagerCS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/consta
import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants';
import { ISdkFactoryContextSync } from '../../sdkFactory/types';
import { usesSegmentsSync } from '../../storages/AbstractSplitsCacheSync';
import { SdkUpdateMetadata } from '../../../types/splitio';

/**
* Expose start / stop mechanism for polling data from services.
Expand Down Expand Up @@ -59,8 +60,8 @@ export function pollingManagerCSFactory(
const mySegmentsSyncTask = mySegmentsSyncTaskFactory(splitApi.fetchMemberships, storage, readiness, settings, matchingKey);

// smart ready
function smartReady() {
if (!readiness.isReady() && !usesSegmentsSync(storage)) readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
function smartReady(metadata: SdkUpdateMetadata) {
if (!readiness.isReady() && !usesSegmentsSync(storage)) readiness.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);
}
if (!usesSegmentsSync(storage)) setTimeout(smartReady, 0);
else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady);
Expand Down
24 changes: 14 additions & 10 deletions src/sync/polling/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ import { IStorageSync } from '../../storages/types';
import { MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../streaming/types';
import { ITask, ISyncTask } from '../types';

/**
* Metadata keys for SDK update events.
*/
export enum SdkUpdateMetadataKeys {
Comment thread
ZamoraEmmanuel marked this conversation as resolved.
Outdated
/**
* The update event emitted when the SDK cache is updated with new data for flags.
*/
FLAGS_UPDATE = 'FLAGS_UPDATE',
/**
* The update event emitted when the SDK cache is updated with new data for segments.
*/
SEGMENTS_UPDATE = 'SEGMENTS_UPDATE'
}

export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }], boolean> { }

export interface ISegmentsSyncTask extends ISyncTask<[fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number], boolean> { }
Expand Down Expand Up @@ -31,13 +45,3 @@ export interface IPollingManagerCS extends IPollingManager {
remove(matchingKey: string): void;
get(matchingKey: string): IMySegmentsSyncTask | undefined
}

export enum SdkUpdateMetadataKeys {
UPDATED_FLAGS = 'updatedFlags',
UPDATED_SEGMENTS = 'updatedSegments'
}

export type SdkUpdateMetadata = {
[SdkUpdateMetadataKeys.UPDATED_FLAGS]?: string[]
[SdkUpdateMetadataKeys.UPDATED_SEGMENTS]?: string[]
}
Loading