From a719bb7e06d67afdfc918f332591519db4eec83b Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 5 Jan 2026 20:23:02 -0300 Subject: [PATCH 1/5] [FME-12059] SDK_UPDATE with metadata --- .../__tests__/readinessManager.spec.ts | 60 ++++++++ src/readiness/readinessManager.ts | 7 +- .../__tests__/TelemetryCacheInRedis.spec.ts | 128 ++++++++++-------- src/sync/polling/types.ts | 10 ++ .../__tests__/segmentChangesUpdater.spec.ts | 52 +++++++ .../__tests__/splitChangesUpdater.spec.ts | 30 +++- .../polling/updaters/segmentChangesUpdater.ts | 2 +- .../polling/updaters/splitChangesUpdater.ts | 13 +- types/splitio.d.ts | 4 + 9 files changed, 237 insertions(+), 69 deletions(-) create mode 100644 src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index 34eaf9a3..69fe4008 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -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 { EventMetadata, SdkUpdateMetadataKeys } from '../../sync/polling/types'; const settings = { startup: { @@ -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: EventMetadata = { + [SdkUpdateMetadataKeys.UPDATED_FLAGS]: ['flag1', 'flag2'] + }; + + let receivedMetadata: EventMetadata | undefined; + readinessManager.gate.on(SDK_UPDATE, (meta: EventMetadata) => { + 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: EventMetadata) => { + 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: EventMetadata = { + [SdkUpdateMetadataKeys.UPDATED_SEGMENTS]: ['segment1', 'segment2'] + }; + + let receivedMetadata: EventMetadata | undefined; + readinessManager.gate.on(SDK_UPDATE, (meta: EventMetadata) => { + receivedMetadata = meta; + }); + + readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED, metadata); + + expect(receivedMetadata).toEqual(metadata); +}); diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 319e843d..48db9a40 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -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(), { @@ -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; @@ -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); diff --git a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts index fb80ffce..f446e7d2 100644 --- a/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/TelemetryCacheInRedis.spec.ts @@ -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); + }); }); diff --git a/src/sync/polling/types.ts b/src/sync/polling/types.ts index 4ff29c83..bb7b6c3e 100644 --- a/src/sync/polling/types.ts +++ b/src/sync/polling/types.ts @@ -31,3 +31,13 @@ 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[] +} diff --git a/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts new file mode 100644 index 00000000..0a85f29a --- /dev/null +++ b/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts @@ -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] }); + }); +}); diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index b93a7176..3bebacc7 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -15,6 +15,7 @@ import { splitNotifications } from '../../../streaming/__tests__/dataMocks'; import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegmentsCacheInMemory'; import { RB_SEGMENT_UPDATE, SPLIT_UPDATE } from '../../../streaming/constants'; import { IN_RULE_BASED_SEGMENT } from '../../../../utils/constants'; +import { SDK_SPLITS_ARRIVED } from '../../../../readiness/constants'; const ARCHIVED_FF = 'ARCHIVED'; @@ -120,6 +121,7 @@ test('splitChangesUpdater / compute splits mutation', () => { expect(splitsMutation.added).toEqual([activeSplitWithSegments]); expect(splitsMutation.removed).toEqual([archivedSplit]); + expect(splitsMutation.names).toEqual([activeSplitWithSegments.name, archivedSplit.name]); expect(Array.from(segments)).toEqual(['A', 'B']); // SDK initialization without sets @@ -129,6 +131,7 @@ test('splitChangesUpdater / compute splits mutation', () => { expect(splitsMutation.added).toEqual([testFFSetsAB, test2FFSetsX]); expect(splitsMutation.removed).toEqual([]); + expect(splitsMutation.names).toEqual([testFFSetsAB.name, test2FFSetsX.name]); expect(Array.from(segments)).toEqual([]); }); @@ -142,24 +145,28 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { // should add it to mutations expect(splitsMutation.added).toEqual([testFFSetsAB]); expect(splitsMutation.removed).toEqual([]); + expect(splitsMutation.names).toEqual([testFFSetsAB.name]); // fetching existing test feature flag removed from set B splitsMutation = computeMutation([testFFRemoveSetB], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([testFFRemoveSetB]); expect(splitsMutation.removed).toEqual([]); + expect(splitsMutation.names).toEqual([testFFRemoveSetB.name]); // fetching existing test feature flag removed from set B splitsMutation = computeMutation([testFFRemoveSetA], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFRemoveSetA]); + expect(splitsMutation.names).toEqual([testFFRemoveSetA.name]); // fetching existing test feature flag removed from set B splitsMutation = computeMutation([testFFEmptySet], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFEmptySet]); + expect(splitsMutation.names).toEqual([testFFEmptySet.name]); // SDK initialization with names: ['test2'] splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; @@ -167,11 +174,13 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFSetsAB]); + expect(splitsMutation.names).toEqual([testFFSetsAB.name]); splitsMutation = computeMutation([test2FFSetsX, testFFEmptySet], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([test2FFSetsX]); expect(splitsMutation.removed).toEqual([testFFEmptySet]); + expect(splitsMutation.names).toEqual([test2FFSetsX.name, testFFEmptySet.name]); }); describe('splitChangesUpdater', () => { @@ -204,6 +213,7 @@ describe('splitChangesUpdater', () => { test('test without payload', async () => { const result = await splitChangesUpdater(); + const updatedFlags = splitChangesMock1.ff.d.map(ff => ff.name); expect(fetchSplitChanges).toBeCalledTimes(1); expect(fetchSplitChanges).lastCalledWith(-1, undefined, undefined, -1); @@ -211,7 +221,7 @@ describe('splitChangesUpdater', () => { expect(updateSplits).lastCalledWith(splitChangesMock1.ff.d, [], splitChangesMock1.ff.t); expect(updateRbSegments).toBeCalledTimes(0); // no rbSegments to update expect(registerSegments).toBeCalledTimes(1); - expect(splitsEmitSpy).toBeCalledWith('state::splits-arrived'); + expect(splitsEmitSpy).toBeCalledWith(SDK_SPLITS_ARRIVED, { updatedFlags }); expect(result).toBe(true); }); @@ -276,7 +286,8 @@ describe('splitChangesUpdater', () => { // emit always if not configured sets for (const setMock of setMocks) { await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index, type: SPLIT_UPDATE })).resolves.toBe(true); - expect(splitsEmitSpy.mock.calls[index][0]).toBe('state::splits-arrived'); + expect(splitsEmitSpy.mock.calls[index][0]).toBe(SDK_SPLITS_ARRIVED); + expect(splitsEmitSpy.mock.calls[index][1]).toEqual({ updatedFlags: [payload.name] }); index++; } @@ -294,4 +305,19 @@ describe('splitChangesUpdater', () => { } }); + + test('test with ff payload - should emit metadata with flag name', async () => { + splitsEmitSpy.mockClear(); + + readinessManager.splits.splitsArrived = false; + storage.splits.clear(); + + const payload = splitNotifications[0].decoded as Pick; + const changeNumber = payload.changeNumber; + + await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber, type: SPLIT_UPDATE })).resolves.toBe(true); + + expect(splitsEmitSpy).toBeCalledWith(SDK_SPLITS_ARRIVED, { updatedFlags: [payload.name] }); + }); + }); diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 7fe5b7b7..cf9fad2e 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -83,7 +83,7 @@ export function segmentChangesUpdaterFactory( // if at least one segment fetch succeeded, mark segments ready if (shouldUpdateFlags.some(update => update) || readyOnAlreadyExistentState) { readyOnAlreadyExistentState = false; - if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { updatedSegments: segmentNames }); } return true; }); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 3a1fc5a7..3a4bf8df 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -54,7 +54,8 @@ export function parseSegments(ruleEntity: ISplit | IRBSegment, matcherType: type interface ISplitMutations { added: T[], - removed: T[] + removed: T[], + names: string[] } /** @@ -88,16 +89,18 @@ export function computeMutation(rules: Array, return rules.reduce((accum, ruleEntity) => { if (ruleEntity.status === 'ACTIVE' && (!filters || matchFilters(ruleEntity as ISplit, filters))) { accum.added.push(ruleEntity); + accum.names.push(ruleEntity.name); parseSegments(ruleEntity).forEach((segmentName: string) => { segments.add(segmentName); }); } else { accum.removed.push(ruleEntity); + accum.names.push(ruleEntity.name); } return accum; - }, { added: [], removed: [] } as ISplitMutations); + }, { added: [], removed: [], names: [] } as ISplitMutations); } /** @@ -165,9 +168,11 @@ export function splitChangesUpdaterFactory( .then((splitChanges: ISplitChangesResponse) => { const usedSegments = new Set(); + let updatedFlags: string[] = []; let ffUpdate: MaybeThenable = false; if (splitChanges.ff) { - const { added, removed } = computeMutation(splitChanges.ff.d, usedSegments, splitFiltersValidation); + const { added, removed, names } = computeMutation(splitChanges.ff.d, usedSegments, splitFiltersValidation); + updatedFlags = names; log.debug(SYNC_SPLITS_UPDATE, [added.length, removed.length]); ffUpdate = splits.update(added, removed, splitChanges.ff.t); } @@ -193,7 +198,7 @@ export function splitChangesUpdaterFactory( .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) .then(emitSplitsArrivedEvent => { // emit SDK events - if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); + if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, { updatedFlags }); return true; }); } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 1a505686..df3decb3 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -3,6 +3,8 @@ import { RedisOptions } from 'ioredis'; import { RequestOptions } from 'http'; +import { SDK_UPDATE } from '../src/readiness/types'; +import { SdkUpdateMetadata } from '../src/sync/polling/types'; export as namespace SplitIO; export = SplitIO; @@ -497,6 +499,7 @@ declare namespace SplitIO { */ interface IEventEmitter { addListener(event: string, listener: (...args: any[]) => void): this; + on(event: SDK_UPDATE, listener: (metadata: SdkUpdateMetadata) => void): this; on(event: string, listener: (...args: any[]) => void): this; once(event: string, listener: (...args: any[]) => void): this; removeListener(event: string, listener: (...args: any[]) => void): this; @@ -510,6 +513,7 @@ declare namespace SplitIO { */ interface EventEmitter extends IEventEmitter { addListener(event: string | symbol, listener: (...args: any[]) => void): this; + on(event: SDK_UPDATE, listener: (metadata: SdkUpdateMetadata) => void): this; on(event: string | symbol, listener: (...args: any[]) => void): this; once(event: string | symbol, listener: (...args: any[]) => void): this; removeListener(event: string | symbol, listener: (...args: any[]) => void): this; From ef651cc1ba3a647b2b8c569d151542c6959c4673 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Tue, 6 Jan 2026 15:48:36 -0300 Subject: [PATCH 2/5] Fix types --- .../__tests__/readinessManager.spec.ts | 16 +++++----- src/readiness/types.ts | 29 ++++++++++--------- src/sync/streaming/types.ts | 5 ++-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index 69fe4008..7acecd03 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -3,7 +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 { EventMetadata, SdkUpdateMetadataKeys } from '../../sync/polling/types'; +import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../../sync/polling/types'; const settings = { startup: { @@ -309,12 +309,12 @@ test('READINESS MANAGER / SDK_UPDATE should emit with metadata', () => { readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - const metadata: EventMetadata = { + const metadata: SdkUpdateMetadata = { [SdkUpdateMetadataKeys.UPDATED_FLAGS]: ['flag1', 'flag2'] }; - let receivedMetadata: EventMetadata | undefined; - readinessManager.gate.on(SDK_UPDATE, (meta: EventMetadata) => { + let receivedMetadata: SdkUpdateMetadata | undefined; + readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => { receivedMetadata = meta; }); @@ -331,7 +331,7 @@ test('READINESS MANAGER / SDK_UPDATE should handle undefined metadata', () => { readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); let receivedMetadata: any; - readinessManager.gate.on(SDK_UPDATE, (meta: EventMetadata) => { + readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => { receivedMetadata = meta; }); @@ -347,12 +347,12 @@ test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', () readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - const metadata: EventMetadata = { + const metadata: SdkUpdateMetadata = { [SdkUpdateMetadataKeys.UPDATED_SEGMENTS]: ['segment1', 'segment2'] }; - let receivedMetadata: EventMetadata | undefined; - readinessManager.gate.on(SDK_UPDATE, (meta: EventMetadata) => { + let receivedMetadata: SdkUpdateMetadata | undefined; + readinessManager.gate.on(SDK_UPDATE, (meta: SdkUpdateMetadata) => { receivedMetadata = meta; }); diff --git a/src/readiness/types.ts b/src/readiness/types.ts index 2de99b43..cca51151 100644 --- a/src/readiness/types.ts +++ b/src/readiness/types.ts @@ -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' @@ -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; once(event: ISplitsEvent, listener: (...args: any[]) => void): this; splitsArrived: boolean splitsCacheLoaded: boolean @@ -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 { diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index fcf5048e..982f40e9 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -1,8 +1,9 @@ import { IMembershipMSUpdateData, IMembershipLSUpdateData, ISegmentUpdateData, ISplitUpdateData, ISplitKillData, INotificationData } from './SSEHandler/types'; import { ITask } from '../types'; -import { IMySegmentsSyncTask } from '../polling/types'; +import { IMySegmentsSyncTask, SdkUpdateMetadata } from '../polling/types'; import SplitIO from '../../../types/splitio'; import { ControlType } from './constants'; +import { SDK_UPDATE } from '../../readiness/types'; // Internal SDK events, subscribed by SyncManager and PushManager export type PUSH_SUBSYSTEM_UP = 'PUSH_SUBSYSTEM_UP' @@ -37,7 +38,7 @@ type IParsedData = */ export interface IPushEventEmitter extends SplitIO.IEventEmitter { once(event: T, listener: (parsedData: IParsedData) => void): this; - on(event: T, listener: (parsedData: IParsedData) => void): this; + on(event: T, listener: (metadata: T extends SDK_UPDATE ? SdkUpdateMetadata : never) => void): this; emit(event: T, parsedData?: IParsedData): boolean; } From f4a698888376d37f9633432896500c45e96be4a4 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 12 Jan 2026 10:39:53 -0300 Subject: [PATCH 3/5] Update metadata properties --- .../__tests__/readinessManager.spec.ts | 6 +++-- src/readiness/readinessManager.ts | 5 ++-- src/readiness/types.ts | 5 ++-- src/sync/polling/types.ts | 12 ++++++---- .../__tests__/segmentChangesUpdater.spec.ts | 3 ++- .../__tests__/splitChangesUpdater.spec.ts | 7 +++--- .../polling/updaters/segmentChangesUpdater.ts | 8 ++++++- .../polling/updaters/splitChangesUpdater.ts | 10 +++++--- src/sync/streaming/types.ts | 5 ++-- types/splitio.d.ts | 24 +++++++++++++++---- 10 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index 7acecd03..afaaaa96 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -310,7 +310,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; @@ -348,7 +349,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: ['segment1', 'segment2'] }; let receivedMetadata: SdkUpdateMetadata | undefined; diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 48db9a40..591f8b02 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -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(), { @@ -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; @@ -99,7 +98,7 @@ export function readinessManagerFactory( } } - function checkIsReadyOrUpdate(metadata: SdkUpdateMetadata) { + function checkIsReadyOrUpdate(metadata: SplitIO.SdkUpdateMetadata) { if (isDestroyed) return; if (isReady) { try { diff --git a/src/readiness/types.ts b/src/readiness/types.ts index cca51151..3f726d64 100644 --- a/src/readiness/types.ts +++ b/src/readiness/types.ts @@ -1,5 +1,4 @@ import SplitIO from '../../types/splitio'; -import { SdkUpdateMetadata } from '../sync/polling/types'; /** Readiness event types */ @@ -23,7 +22,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 @@ -39,7 +38,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 } diff --git a/src/sync/polling/types.ts b/src/sync/polling/types.ts index bb7b6c3e..3b91476c 100644 --- a/src/sync/polling/types.ts +++ b/src/sync/polling/types.ts @@ -33,11 +33,13 @@ export interface IPollingManagerCS extends IPollingManager { } export enum SdkUpdateMetadataKeys { - UPDATED_FLAGS = 'updatedFlags', - UPDATED_SEGMENTS = 'updatedSegments' + FLAGS_UPDATE = 'FLAGS_UPDATE', + SEGMENTS_UPDATE = 'SEGMENTS_UPDATE' } - +/** + * SdkUpdateMetadata type for polling updaters + */ export type SdkUpdateMetadata = { - [SdkUpdateMetadataKeys.UPDATED_FLAGS]?: string[] - [SdkUpdateMetadataKeys.UPDATED_SEGMENTS]?: string[] + type: SdkUpdateMetadataKeys.FLAGS_UPDATE | SdkUpdateMetadataKeys.SEGMENTS_UPDATE + names: string[] } diff --git a/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts index 0a85f29a..d3a2bedf 100644 --- a/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts @@ -7,6 +7,7 @@ import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; import { ISegmentChangesFetcher } from '../../fetchers/types'; import { ISegmentChangesResponse } from '../../../../dtos/types'; import { SDK_SEGMENTS_ARRIVED } from '../../../../readiness/constants'; +import { SdkUpdateMetadataKeys } from '../../types'; describe('segmentChangesUpdater', () => { const segments = new SegmentsCacheInMemory(); @@ -47,6 +48,6 @@ describe('segmentChangesUpdater', () => { await segmentChangesUpdater(undefined, segmentName); expect(updateSegments).toHaveBeenCalledWith(segmentName, segmentChange.added, segmentChange.removed, segmentChange.till); - expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { updatedSegments: [segmentName] }); + expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [segmentName] }); }); }); diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 3bebacc7..6b59f6fb 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -16,6 +16,7 @@ import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegment import { RB_SEGMENT_UPDATE, SPLIT_UPDATE } from '../../../streaming/constants'; import { IN_RULE_BASED_SEGMENT } from '../../../../utils/constants'; import { SDK_SPLITS_ARRIVED } from '../../../../readiness/constants'; +import { SdkUpdateMetadataKeys } from '../../types'; const ARCHIVED_FF = 'ARCHIVED'; @@ -221,7 +222,7 @@ describe('splitChangesUpdater', () => { expect(updateSplits).lastCalledWith(splitChangesMock1.ff.d, [], splitChangesMock1.ff.t); expect(updateRbSegments).toBeCalledTimes(0); // no rbSegments to update expect(registerSegments).toBeCalledTimes(1); - expect(splitsEmitSpy).toBeCalledWith(SDK_SPLITS_ARRIVED, { updatedFlags }); + expect(splitsEmitSpy).toBeCalledWith(SDK_SPLITS_ARRIVED, { type: SdkUpdateMetadataKeys.FLAGS_UPDATE, names: updatedFlags }); expect(result).toBe(true); }); @@ -287,7 +288,7 @@ describe('splitChangesUpdater', () => { for (const setMock of setMocks) { await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index, type: SPLIT_UPDATE })).resolves.toBe(true); expect(splitsEmitSpy.mock.calls[index][0]).toBe(SDK_SPLITS_ARRIVED); - expect(splitsEmitSpy.mock.calls[index][1]).toEqual({ updatedFlags: [payload.name] }); + expect(splitsEmitSpy.mock.calls[index][1]).toEqual({ type: SdkUpdateMetadataKeys.FLAGS_UPDATE, names: [payload.name] }); index++; } @@ -317,7 +318,7 @@ describe('splitChangesUpdater', () => { await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber, type: SPLIT_UPDATE })).resolves.toBe(true); - expect(splitsEmitSpy).toBeCalledWith(SDK_SPLITS_ARRIVED, { updatedFlags: [payload.name] }); + expect(splitsEmitSpy).toBeCalledWith(SDK_SPLITS_ARRIVED, { type: SdkUpdateMetadataKeys.FLAGS_UPDATE, names: [payload.name] }); }); }); diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index cf9fad2e..67179c69 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -5,6 +5,8 @@ import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants'; import { timeout } from '../../../utils/promise/timeout'; +import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../types'; + type ISegmentChangesUpdater = (fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number) => Promise @@ -83,7 +85,11 @@ export function segmentChangesUpdaterFactory( // if at least one segment fetch succeeded, mark segments ready if (shouldUpdateFlags.some(update => update) || readyOnAlreadyExistentState) { readyOnAlreadyExistentState = false; - if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { updatedSegments: segmentNames }); + const metadata: SdkUpdateMetadata = { + type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, + names: segmentNames + }; + if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED, metadata); } return true; }); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 3a4bf8df..98b3bf37 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -10,6 +10,7 @@ import { startsWith } from '../../../utils/lang'; import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; import { SPLIT_UPDATE } from '../../streaming/constants'; +import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../types'; export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string }; type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise @@ -89,15 +90,14 @@ export function computeMutation(rules: Array, return rules.reduce((accum, ruleEntity) => { if (ruleEntity.status === 'ACTIVE' && (!filters || matchFilters(ruleEntity as ISplit, filters))) { accum.added.push(ruleEntity); - accum.names.push(ruleEntity.name); parseSegments(ruleEntity).forEach((segmentName: string) => { segments.add(segmentName); }); } else { accum.removed.push(ruleEntity); - accum.names.push(ruleEntity.name); } + accum.names.push(ruleEntity.name); return accum; }, { added: [], removed: [], names: [] } as ISplitMutations); @@ -197,8 +197,12 @@ export function splitChangesUpdaterFactory( return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments)))) .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) .then(emitSplitsArrivedEvent => { + const metadata: SdkUpdateMetadata = { + type: SdkUpdateMetadataKeys.FLAGS_UPDATE, + names: updatedFlags + }; // emit SDK events - if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, { updatedFlags }); + if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, metadata); return true; }); } diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index 982f40e9..fcf5048e 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -1,9 +1,8 @@ import { IMembershipMSUpdateData, IMembershipLSUpdateData, ISegmentUpdateData, ISplitUpdateData, ISplitKillData, INotificationData } from './SSEHandler/types'; import { ITask } from '../types'; -import { IMySegmentsSyncTask, SdkUpdateMetadata } from '../polling/types'; +import { IMySegmentsSyncTask } from '../polling/types'; import SplitIO from '../../../types/splitio'; import { ControlType } from './constants'; -import { SDK_UPDATE } from '../../readiness/types'; // Internal SDK events, subscribed by SyncManager and PushManager export type PUSH_SUBSYSTEM_UP = 'PUSH_SUBSYSTEM_UP' @@ -38,7 +37,7 @@ type IParsedData = */ export interface IPushEventEmitter extends SplitIO.IEventEmitter { once(event: T, listener: (parsedData: IParsedData) => void): this; - on(event: T, listener: (metadata: T extends SDK_UPDATE ? SdkUpdateMetadata : never) => void): this; + on(event: T, listener: (parsedData: IParsedData) => void): this; emit(event: T, parsedData?: IParsedData): boolean; } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index df3decb3..e23ae462 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -3,8 +3,6 @@ import { RedisOptions } from 'ioredis'; import { RequestOptions } from 'http'; -import { SDK_UPDATE } from '../src/readiness/types'; -import { SdkUpdateMetadata } from '../src/sync/polling/types'; export as namespace SplitIO; export = SplitIO; @@ -494,12 +492,28 @@ declare namespace SplitIO { removeItem(key: string): void | Promise; } + /** + * Metadata keys for SDK update events. + */ + enum SdkUpdateMetadataKeys { + FLAGS_UPDATE = 'FLAGS_UPDATE', + SEGMENTS_UPDATE = 'SEGMENTS_UPDATE' + } + + /** + * List of modified flags or segments + * when a sdk update event is emitted. + */ + type SdkUpdateMetadata = { + type: SdkUpdateMetadataKeys.FLAGS_UPDATE | SdkUpdateMetadataKeys.SEGMENTS_UPDATE + names: string[] + } + /** * EventEmitter interface based on a subset of the Node.js EventEmitter methods. */ interface IEventEmitter { addListener(event: string, listener: (...args: any[]) => void): this; - on(event: SDK_UPDATE, listener: (metadata: SdkUpdateMetadata) => void): this; on(event: string, listener: (...args: any[]) => void): this; once(event: string, listener: (...args: any[]) => void): this; removeListener(event: string, listener: (...args: any[]) => void): this; @@ -512,9 +526,11 @@ declare namespace SplitIO { * @see {@link https://nodejs.org/api/events.html} */ interface EventEmitter extends IEventEmitter { + addListener(event: EventConsts['SDK_UPDATE'], listener: (metadata: SdkUpdateMetadata) => void): this; addListener(event: string | symbol, listener: (...args: any[]) => void): this; - on(event: SDK_UPDATE, listener: (metadata: SdkUpdateMetadata) => void): this; + on(event: EventConsts['SDK_UPDATE'], listener: (metadata: SdkUpdateMetadata) => void): this; on(event: string | symbol, listener: (...args: any[]) => void): this; + once(event: EventConsts['SDK_UPDATE'], listener: (metadata: SdkUpdateMetadata) => void): this; once(event: string | symbol, listener: (...args: any[]) => void): this; removeListener(event: string | symbol, listener: (...args: any[]) => void): this; off(event: string | symbol, listener: (...args: any[]) => void): this; From 631366d178f4f23783b513f4e02a02d7b701ac2d Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 12 Jan 2026 11:01:15 -0300 Subject: [PATCH 4/5] Move enum and type to namespace --- .../__tests__/readinessManager.spec.ts | 2 +- src/sync/polling/types.ts | 12 ------------ .../__tests__/segmentChangesUpdater.spec.ts | 2 +- .../__tests__/splitChangesUpdater.spec.ts | 2 +- .../polling/updaters/segmentChangesUpdater.ts | 2 +- .../polling/updaters/splitChangesUpdater.ts | 2 +- types/splitio.d.ts | 17 ++++++++++++++--- 7 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index afaaaa96..40fe34a0 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -3,7 +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'; +import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../../../types/splitio'; const settings = { startup: { diff --git a/src/sync/polling/types.ts b/src/sync/polling/types.ts index 3b91476c..4ff29c83 100644 --- a/src/sync/polling/types.ts +++ b/src/sync/polling/types.ts @@ -31,15 +31,3 @@ export interface IPollingManagerCS extends IPollingManager { remove(matchingKey: string): void; get(matchingKey: string): IMySegmentsSyncTask | undefined } - -export enum SdkUpdateMetadataKeys { - FLAGS_UPDATE = 'FLAGS_UPDATE', - SEGMENTS_UPDATE = 'SEGMENTS_UPDATE' -} -/** - * SdkUpdateMetadata type for polling updaters - */ -export type SdkUpdateMetadata = { - type: SdkUpdateMetadataKeys.FLAGS_UPDATE | SdkUpdateMetadataKeys.SEGMENTS_UPDATE - names: string[] -} diff --git a/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts index d3a2bedf..81113692 100644 --- a/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts @@ -7,7 +7,7 @@ import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; import { ISegmentChangesFetcher } from '../../fetchers/types'; import { ISegmentChangesResponse } from '../../../../dtos/types'; import { SDK_SEGMENTS_ARRIVED } from '../../../../readiness/constants'; -import { SdkUpdateMetadataKeys } from '../../types'; +import { SdkUpdateMetadataKeys } from '../../../../../types/splitio'; describe('segmentChangesUpdater', () => { const segments = new SegmentsCacheInMemory(); diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 6b59f6fb..3ea8c740 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -16,7 +16,7 @@ import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegment import { RB_SEGMENT_UPDATE, SPLIT_UPDATE } from '../../../streaming/constants'; import { IN_RULE_BASED_SEGMENT } from '../../../../utils/constants'; import { SDK_SPLITS_ARRIVED } from '../../../../readiness/constants'; -import { SdkUpdateMetadataKeys } from '../../types'; +import { SdkUpdateMetadataKeys } from '../../../../../types/splitio'; const ARCHIVED_FF = 'ARCHIVED'; diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 67179c69..e29dc551 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -5,7 +5,7 @@ import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants'; import { timeout } from '../../../utils/promise/timeout'; -import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../types'; +import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../../../../types/splitio'; type ISegmentChangesUpdater = (fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number) => Promise diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 98b3bf37..5e3bf986 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -10,7 +10,7 @@ import { startsWith } from '../../../utils/lang'; import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; import { SPLIT_UPDATE } from '../../streaming/constants'; -import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../types'; +import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../../../../types/splitio'; export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string }; type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise diff --git a/types/splitio.d.ts b/types/splitio.d.ts index e23ae462..e908bc13 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -495,17 +495,28 @@ declare namespace SplitIO { /** * Metadata keys for SDK update events. */ - enum SdkUpdateMetadataKeys { + const enum SdkUpdateMetadataKeys { + /** + * 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' } /** - * List of modified flags or segments - * when a sdk update event is emitted. + * Metadata for the update event emitted when the SDK cache is updated with new data for flags or segments. */ type SdkUpdateMetadata = { + /** + * The type of update event. + */ type: SdkUpdateMetadataKeys.FLAGS_UPDATE | SdkUpdateMetadataKeys.SEGMENTS_UPDATE + /** + * The names of the flags or segments that were updated. + */ names: string[] } From 6f054c99db10eca7991f3cac661b7dd947d988d7 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 12 Jan 2026 15:09:46 -0300 Subject: [PATCH 5/5] Remove names when segments update --- src/readiness/__tests__/readinessManager.spec.ts | 2 +- .../polling/updaters/__tests__/segmentChangesUpdater.spec.ts | 2 +- src/sync/polling/updaters/segmentChangesUpdater.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/readiness/__tests__/readinessManager.spec.ts b/src/readiness/__tests__/readinessManager.spec.ts index 40fe34a0..4af32c80 100644 --- a/src/readiness/__tests__/readinessManager.spec.ts +++ b/src/readiness/__tests__/readinessManager.spec.ts @@ -350,7 +350,7 @@ test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', () const metadata: SdkUpdateMetadata = { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, - names: ['segment1', 'segment2'] + names: [] }; let receivedMetadata: SdkUpdateMetadata | undefined; diff --git a/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts index 81113692..acef0f94 100644 --- a/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts @@ -48,6 +48,6 @@ describe('segmentChangesUpdater', () => { await segmentChangesUpdater(undefined, segmentName); expect(updateSegments).toHaveBeenCalledWith(segmentName, segmentChange.added, segmentChange.removed, segmentChange.till); - expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [segmentName] }); + expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] }); }); }); diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index e29dc551..5bda5d9f 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -87,7 +87,7 @@ export function segmentChangesUpdaterFactory( readyOnAlreadyExistentState = false; const metadata: SdkUpdateMetadata = { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, - names: segmentNames + names: [] }; if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED, metadata); }