Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
60 changes: 60 additions & 0 deletions src/readiness/__tests__/readinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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';

const settings = {
startup: {
Expand Down Expand Up @@ -300,3 +301,62 @@ 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 = {
[SdkUpdateMetadataKeys.UPDATED_FLAGS]: ['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 = {
[SdkUpdateMetadataKeys.UPDATED_SEGMENTS]: ['segment1', 'segment2']
};

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

readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);

expect(receivedMetadata).toEqual(metadata);
});
7 changes: 4 additions & 3 deletions src/readiness/readinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 @@ -15,7 +16,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: SdkUpdateMetadata, isSplitKill: boolean) => { if (!isSplitKill) splitsEventEmitter.splitsArrived = true; });
splitsEventEmitter.once(SDK_SPLITS_CACHE_LOADED, () => { splitsEventEmitter.splitsCacheLoaded = true; });

return splitsEventEmitter;
Expand Down Expand Up @@ -98,12 +99,12 @@ export function readinessManagerFactory(
}
}

function checkIsReadyOrUpdate(diff: any) {
function checkIsReadyOrUpdate(metadata: 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 Down
29 changes: 16 additions & 13 deletions src/readiness/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import SplitIO from '../../types/splitio';
import { SdkUpdateMetadata } from '../sync/polling/types';

/** 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
}
/** Splits data emitter */

type SDK_SPLITS_ARRIVED = 'state::splits-arrived'
Expand All @@ -9,6 +23,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;
Comment thread
ZamoraEmmanuel marked this conversation as resolved.
Outdated
once(event: ISplitsEvent, listener: (...args: any[]) => void): this;
splitsArrived: boolean
splitsCacheLoaded: boolean
Expand All @@ -24,23 +39,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: 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
128 changes: 69 additions & 59 deletions src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,78 +10,88 @@ const latencyKey = `${prefix}.telemetry.latencies`;
const initKey = `${prefix}.telemetry.init`;
const fieldVersionablePrefix = `${metadata.s}/${metadata.n}/${metadata.i}`;

test('TELEMETRY CACHE IN REDIS', async () => {
describe('TELEMETRY CACHE IN REDIS', () => {
let connection: RedisAdapter;
let cache: TelemetryCacheInRedis;
let keysBuilder: KeyBuilderSS;

const keysBuilder = new KeyBuilderSS(prefix, metadata);
const connection = new RedisAdapter(loggerMock);
const cache = new TelemetryCacheInRedis(loggerMock, keysBuilder, connection);
beforeEach(async () => {
keysBuilder = new KeyBuilderSS(prefix, metadata);
connection = new RedisAdapter(loggerMock);
cache = new TelemetryCacheInRedis(loggerMock, keysBuilder, connection);

// recordException
expect(await cache.recordException('tr')).toBe(1);
expect(await cache.recordException('tr')).toBe(2);
expect(await cache.recordException('tcfs')).toBe(1);
await connection.del(exceptionKey);
await connection.del(latencyKey);
await connection.del(initKey);
});

expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/track')).toBe('2');
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/treatment')).toBe(null);
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/treatmentsWithConfigByFlagSets')).toBe('1');
test('TELEMETRY CACHE IN REDIS', async () => {

// recordLatency
expect(await cache.recordLatency('tr', 1.6)).toBe(1);
expect(await cache.recordLatency('tr', 1.6)).toBe(2);
expect(await cache.recordLatency('tfs', 1.6)).toBe(1);
// recordException
expect(await cache.recordException('tr')).toBe(1);
expect(await cache.recordException('tr')).toBe(2);
expect(await cache.recordException('tcfs')).toBe(1);

expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/track/2')).toBe('2');
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/treatment/2')).toBe(null);
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/treatmentsByFlagSets/2')).toBe('1');
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/track')).toBe('2');
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/treatment')).toBe(null);
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/treatmentsWithConfigByFlagSets')).toBe('1');

// recordConfig
expect(await cache.recordConfig()).toBe(1);
expect(JSON.parse(await connection.hget(initKey, fieldVersionablePrefix) as string)).toEqual({
oM: 1,
st: 'redis',
aF: 0,
rF: 0
});
// recordLatency
expect(await cache.recordLatency('tr', 1.6)).toBe(1);
expect(await cache.recordLatency('tr', 1.6)).toBe(2);
expect(await cache.recordLatency('tfs', 1.6)).toBe(1);

// popLatencies
const latencies = await cache.popLatencies();
latencies.forEach((latency, m) => {
expect(JSON.parse(m)).toEqual(metadata);
expect(latency).toEqual({
tfs: [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
tr: [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
});
});
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/track/2')).toBe(null);

// popExceptions
const exceptions = await cache.popExceptions();
exceptions.forEach((exception, m) => {
expect(JSON.parse(m)).toEqual(metadata);
expect(exception).toEqual({
tcfs: 1,
tr: 2,
});
});
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/track')).toBe(null);
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/track/2')).toBe('2');
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/treatment/2')).toBe(null);
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/treatmentsByFlagSets/2')).toBe('1');

// popConfig
const configs = await cache.popConfigs();
configs.forEach((config, m) => {
expect(JSON.parse(m)).toEqual(metadata);
expect(config).toEqual({
// recordConfig
expect(await cache.recordConfig()).toBe(1);
expect(JSON.parse(await connection.hget(initKey, fieldVersionablePrefix) as string)).toEqual({
oM: 1,
st: 'redis',
aF: 0,
rF: 0
});
});
expect(await connection.hget(initKey, fieldVersionablePrefix)).toBe(null);

// pops when there is no data
expect((await cache.popLatencies()).size).toBe(0);
expect((await cache.popExceptions()).size).toBe(0);
expect((await cache.popConfigs()).size).toBe(0);
// popLatencies
const latencies = await cache.popLatencies();
latencies.forEach((latency, m) => {
expect(JSON.parse(m)).toEqual(metadata);
expect(latency).toEqual({
tfs: [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
tr: [0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
});
});
expect(await connection.hget(latencyKey, fieldVersionablePrefix + '/track/2')).toBe(null);

// popExceptions
const exceptions = await cache.popExceptions();
exceptions.forEach((exception, m) => {
expect(JSON.parse(m)).toEqual(metadata);
expect(exception).toEqual({
tcfs: 1,
tr: 2,
});
});
expect(await connection.hget(exceptionKey, fieldVersionablePrefix + '/track')).toBe(null);

await connection.disconnect();
// popConfig
const configs = await cache.popConfigs();
configs.forEach((config, m) => {
expect(JSON.parse(m)).toEqual(metadata);
expect(config).toEqual({
oM: 1,
st: 'redis',
aF: 0,
rF: 0
});
});
expect(await connection.hget(initKey, fieldVersionablePrefix)).toBe(null);

// pops when there is no data
expect((await cache.popLatencies()).size).toBe(0);
expect((await cache.popExceptions()).size).toBe(0);
expect((await cache.popConfigs()).size).toBe(0);
});
});
10 changes: 10 additions & 0 deletions src/sync/polling/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ export interface IPollingManagerCS extends IPollingManager {
remove(matchingKey: string): void;
get(matchingKey: string): IMySegmentsSyncTask | undefined
}

export enum SdkUpdateMetadataKeys {
Comment thread
ZamoraEmmanuel marked this conversation as resolved.
Outdated
UPDATED_FLAGS = 'updatedFlags',
UPDATED_SEGMENTS = 'updatedSegments'
}

export type SdkUpdateMetadata = {
[SdkUpdateMetadataKeys.UPDATED_FLAGS]?: string[]
[SdkUpdateMetadataKeys.UPDATED_SEGMENTS]?: string[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { readinessManagerFactory } from '../../../../readiness/readinessManager';
import { SegmentsCacheInMemory } from '../../../../storages/inMemory/SegmentsCacheInMemory';
import { segmentChangesUpdaterFactory } from '../segmentChangesUpdater';
import { fullSettings } from '../../../../utils/settingsValidation/__tests__/settings.mocks';
import { EventEmitter } from '../../../../utils/MinEvents';
import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock';
import { ISegmentChangesFetcher } from '../../fetchers/types';
import { ISegmentChangesResponse } from '../../../../dtos/types';
import { SDK_SEGMENTS_ARRIVED } from '../../../../readiness/constants';

describe('segmentChangesUpdater', () => {
const segments = new SegmentsCacheInMemory();
const updateSegments = jest.spyOn(segments, 'update');

const readinessManager = readinessManagerFactory(EventEmitter, fullSettings);
const segmentsEmitSpy = jest.spyOn(readinessManager.segments, 'emit');

beforeEach(() => {
jest.clearAllMocks();
segments.clear();
readinessManager.segments.segmentsArrived = false;
});

test('test with segments update - should emit updatedSegments and NOT updatedFlags', async () => {
const segmentName = 'test-segment';
const segmentChange: ISegmentChangesResponse = {
name: segmentName,
added: ['key1', 'key2'],
removed: [],
since: -1,
till: 123
};

const mockSegmentChangesFetcher: ISegmentChangesFetcher = jest.fn().mockResolvedValue([segmentChange]);

const segmentChangesUpdater = segmentChangesUpdaterFactory(
loggerMock,
mockSegmentChangesFetcher,
segments,
readinessManager,
1000,
1
);

segments.registerSegments([segmentName]);

await segmentChangesUpdater(undefined, segmentName);

expect(updateSegments).toHaveBeenCalledWith(segmentName, segmentChange.added, segmentChange.removed, segmentChange.till);
expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { updatedSegments: [segmentName] });
});
});
Loading