Skip to content

Commit 27dc057

Browse files
Add ready metadata and polling
1 parent cb807b5 commit 27dc057

11 files changed

Lines changed: 435 additions & 11 deletions

File tree

src/readiness/__tests__/readinessManager.spec.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventEmitter } from '../../utils/MinEvents';
33
import { IReadinessManager } from '../types';
44
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';
55
import { ISettings } from '../../types';
6-
import { SdkUpdateMetadata, SdkUpdateMetadataKeys } from '../../../types/splitio';
6+
import { SdkUpdateMetadata, SdkUpdateMetadataKeys, SdkReadyMetadata } from '../../../types/splitio';
77

88
const settings = {
99
startup: {
@@ -362,3 +362,81 @@ test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', ()
362362

363363
expect(receivedMetadata).toEqual(metadata);
364364
});
365+
366+
test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when cache is loaded', () => {
367+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
368+
369+
let receivedMetadata: SdkReadyMetadata | undefined;
370+
readinessManager.gate.on(SDK_READY_FROM_CACHE, (meta: SdkReadyMetadata) => {
371+
receivedMetadata = meta;
372+
});
373+
374+
// Emit cache loaded event
375+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
376+
377+
expect(receivedMetadata).toBeDefined();
378+
expect(receivedMetadata!.initialCacheLoad).toBe(true);
379+
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
380+
// Allow small timing difference (up to 10ms)
381+
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
382+
});
383+
384+
test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when SDK becomes ready without cache', () => {
385+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
386+
387+
let receivedMetadata: SdkReadyMetadata | undefined;
388+
readinessManager.gate.on(SDK_READY_FROM_CACHE, (meta: SdkReadyMetadata) => {
389+
receivedMetadata = meta;
390+
});
391+
392+
// Make SDK ready without cache first
393+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
394+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
395+
396+
expect(receivedMetadata).toBeDefined();
397+
expect(receivedMetadata!.initialCacheLoad).toBe(false);
398+
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
399+
// Allow small timing difference (up to 10ms)
400+
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
401+
});
402+
403+
test('READINESS MANAGER / SDK_READY should emit with metadata when ready from cache', () => {
404+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
405+
406+
// First emit cache loaded
407+
readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
408+
409+
let receivedMetadata: SdkReadyMetadata | undefined;
410+
readinessManager.gate.on(SDK_READY, (meta: SdkReadyMetadata) => {
411+
receivedMetadata = meta;
412+
});
413+
414+
// Make SDK ready
415+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
416+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
417+
418+
expect(receivedMetadata).toBeDefined();
419+
expect(receivedMetadata!.initialCacheLoad).toBe(true); // Was ready from cache first
420+
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
421+
// Allow small timing difference (up to 10ms)
422+
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
423+
});
424+
425+
test('READINESS MANAGER / SDK_READY should emit with metadata when ready without cache', () => {
426+
const readinessManager = readinessManagerFactory(EventEmitter, settings);
427+
428+
let receivedMetadata: SdkReadyMetadata | undefined;
429+
readinessManager.gate.on(SDK_READY, (meta: SdkReadyMetadata) => {
430+
receivedMetadata = meta;
431+
});
432+
433+
// Make SDK ready without cache
434+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
435+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
436+
437+
expect(receivedMetadata).toBeDefined();
438+
expect(receivedMetadata!.initialCacheLoad).toBe(false); // Was not ready from cache
439+
expect(receivedMetadata!.lastUpdateTimestamp).toBeGreaterThan(0);
440+
// Allow small timing difference (up to 10ms)
441+
expect(receivedMetadata!.lastUpdateTimestamp).toBeLessThanOrEqual(Date.now() + 10);
442+
});

src/readiness/readinessManager.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,11 @@ export function readinessManagerFactory(
9090
if (!isReady && !isDestroyed) {
9191
try {
9292
syncLastUpdate();
93-
gate.emit(SDK_READY_FROM_CACHE, isReady);
93+
const metadata: SplitIO.SdkReadyMetadata = {
94+
initialCacheLoad: true,
95+
lastUpdateTimestamp: lastUpdate
96+
};
97+
gate.emit(SDK_READY_FROM_CACHE, metadata);
9498
} catch (e) {
9599
// throws user callback exceptions in next tick
96100
setTimeout(() => { throw e; }, 0);
@@ -114,11 +118,20 @@ export function readinessManagerFactory(
114118
isReady = true;
115119
try {
116120
syncLastUpdate();
121+
const wasReadyFromCache = isReadyFromCache;
117122
if (!isReadyFromCache) {
118123
isReadyFromCache = true;
119-
gate.emit(SDK_READY_FROM_CACHE, isReady);
124+
const metadataFromCache: SplitIO.SdkReadyMetadata = {
125+
initialCacheLoad: false,
126+
lastUpdateTimestamp: lastUpdate
127+
};
128+
gate.emit(SDK_READY_FROM_CACHE, metadataFromCache);
120129
}
121-
gate.emit(SDK_READY);
130+
const metadataReady: SplitIO.SdkReadyMetadata = {
131+
initialCacheLoad: wasReadyFromCache,
132+
lastUpdateTimestamp: lastUpdate
133+
};
134+
gate.emit(SDK_READY, metadataReady);
122135
} catch (e) {
123136
// throws user callback exceptions in next tick
124137
setTimeout(() => { throw e; }, 0);

src/readiness/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ export type IReadinessEvent = SDK_READY_TIMED_OUT | SDK_READY | SDK_READY_FROM_C
1212

1313
export interface IReadinessEventEmitter extends SplitIO.IEventEmitter {
1414
emit(event: IReadinessEvent, ...args: any[]): boolean
15+
on(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
16+
on(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
17+
on(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
18+
on(event: string | symbol, listener: (...args: any[]) => void): this;
19+
once(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
20+
once(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
21+
once(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
22+
once(event: string | symbol, listener: (...args: any[]) => void): this;
23+
addListener(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
24+
addListener(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
25+
addListener(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
26+
addListener(event: string | symbol, listener: (...args: any[]) => void): this;
1527
}
1628
/** Splits data emitter */
1729

src/sync/offline/syncTasks/fromObjectSyncTask.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ISettings } from '../../../types';
99
import { CONTROL } from '../../../utils/constants';
1010
import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants';
1111
import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants';
12+
import { SdkUpdateMetadataKeys } from '../../../../types/splitio';
1213

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

6061
if (startingUp) {
6162
startingUp = false;
6263
Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
6364
// Emits SDK_READY_FROM_CACHE
6465
if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
6566
// Emits SDK_READY
66-
readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
67+
readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
6768
});
6869
}
6970
return true;

src/sync/polling/pollingManagerCS.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/consta
99
import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants';
1010
import { ISdkFactoryContextSync } from '../../sdkFactory/types';
1111
import { usesSegmentsSync } from '../../storages/AbstractSplitsCacheSync';
12+
import { SdkUpdateMetadata } from '../../../types/splitio';
1213

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

6162
// smart ready
62-
function smartReady() {
63-
if (!readiness.isReady() && !usesSegmentsSync(storage)) readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
63+
function smartReady(metadata: SdkUpdateMetadata) {
64+
if (!readiness.isReady() && !usesSegmentsSync(storage)) readiness.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);
6465
}
6566
if (!usesSegmentsSync(storage)) setTimeout(smartReady, 0);
6667
else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { readinessManagerFactory } from '../../../../readiness/readinessManager';
2+
import { MySegmentsCacheInMemory } from '../../../../storages/inMemory/MySegmentsCacheInMemory';
3+
import { mySegmentsUpdaterFactory } from '../mySegmentsUpdater';
4+
import { fullSettings } from '../../../../utils/settingsValidation/__tests__/settings.mocks';
5+
import { EventEmitter } from '../../../../utils/MinEvents';
6+
import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock';
7+
import { IMySegmentsFetcher } from '../../fetchers/types';
8+
import { IMembershipsResponse } from '../../../../dtos/types';
9+
import { SDK_SEGMENTS_ARRIVED } from '../../../../readiness/constants';
10+
import { SdkUpdateMetadataKeys } from '../../../../../types/splitio';
11+
import { MySegmentsData } from '../../types';
12+
import { MEMBERSHIPS_MS_UPDATE } from '../../../streaming/constants';
13+
import { IStorageSync } from '../../../../storages/types';
14+
import { SplitsCacheInMemory } from '../../../../storages/inMemory/SplitsCacheInMemory';
15+
import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegmentsCacheInMemory';
16+
17+
describe('mySegmentsUpdater', () => {
18+
const segments = new MySegmentsCacheInMemory();
19+
const largeSegments = new MySegmentsCacheInMemory();
20+
const splits = new SplitsCacheInMemory();
21+
const rbSegments = new RBSegmentsCacheInMemory();
22+
const storage: IStorageSync = {
23+
segments,
24+
largeSegments,
25+
splits,
26+
rbSegments,
27+
impressions: {} as any,
28+
events: {} as any,
29+
impressionCounts: {} as any,
30+
telemetry: undefined,
31+
uniqueKeys: {} as any,
32+
save: () => {},
33+
destroy: () => {}
34+
};
35+
const readinessManager = readinessManagerFactory(EventEmitter, fullSettings);
36+
const segmentsEmitSpy = jest.spyOn(readinessManager.segments, 'emit');
37+
38+
beforeEach(() => {
39+
jest.clearAllMocks();
40+
storage.segments.clear();
41+
readinessManager.segments.segmentsArrived = false;
42+
});
43+
44+
test('test with mySegments update - should emit SEGMENTS_UPDATE metadata', async () => {
45+
const mockMySegmentsFetcher: IMySegmentsFetcher = jest.fn().mockResolvedValue({
46+
ms: { 'segment1': true, 'segment2': true },
47+
ls: {}
48+
} as IMembershipsResponse);
49+
50+
const mySegmentsUpdater = mySegmentsUpdaterFactory(
51+
loggerMock,
52+
mockMySegmentsFetcher,
53+
storage,
54+
readinessManager.segments,
55+
1000,
56+
1,
57+
'test-key'
58+
);
59+
60+
await mySegmentsUpdater();
61+
62+
expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
63+
});
64+
65+
test('test with mySegments data payload - should emit SEGMENTS_UPDATE metadata', async () => {
66+
const segmentsData: MySegmentsData = {
67+
type: MEMBERSHIPS_MS_UPDATE,
68+
cn: 123,
69+
added: ['segment1', 'segment2'],
70+
removed: []
71+
};
72+
73+
const mySegmentsUpdater = mySegmentsUpdaterFactory(
74+
loggerMock,
75+
jest.fn().mockResolvedValue({ ms: {}, ls: {} } as IMembershipsResponse),
76+
storage,
77+
readinessManager.segments,
78+
1000,
79+
1,
80+
'test-key'
81+
);
82+
83+
await mySegmentsUpdater(segmentsData);
84+
85+
expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
86+
});
87+
88+
test('test with empty mySegments - should still emit SEGMENTS_UPDATE metadata', async () => {
89+
const mockMySegmentsFetcher: IMySegmentsFetcher = jest.fn().mockResolvedValue({
90+
ms: {},
91+
ls: {}
92+
} as IMembershipsResponse);
93+
94+
const mySegmentsUpdater = mySegmentsUpdaterFactory(
95+
loggerMock,
96+
mockMySegmentsFetcher,
97+
storage,
98+
readinessManager.segments,
99+
1000,
100+
1,
101+
'test-key'
102+
);
103+
104+
await mySegmentsUpdater();
105+
106+
expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
107+
});
108+
});

src/sync/polling/updaters/__tests__/segmentChangesUpdater.spec.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,86 @@ describe('segmentChangesUpdater', () => {
5050
expect(updateSegments).toHaveBeenCalledWith(segmentName, segmentChange.added, segmentChange.removed, segmentChange.till);
5151
expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
5252
});
53+
54+
test('test with multiple segments update - should emit SEGMENTS_UPDATE metadata once', async () => {
55+
const segment1 = 'segment1';
56+
const segment2 = 'segment2';
57+
const segment3 = 'segment3';
58+
59+
const segmentChange1: ISegmentChangesResponse = {
60+
name: segment1,
61+
added: ['key1'],
62+
removed: [],
63+
since: -1,
64+
till: 100
65+
};
66+
67+
const segmentChange2: ISegmentChangesResponse = {
68+
name: segment2,
69+
added: ['key2'],
70+
removed: [],
71+
since: -1,
72+
till: 101
73+
};
74+
75+
const segmentChange3: ISegmentChangesResponse = {
76+
name: segment3,
77+
added: ['key3'],
78+
removed: [],
79+
since: -1,
80+
till: 102
81+
};
82+
83+
const mockSegmentChangesFetcher: ISegmentChangesFetcher = jest.fn().mockResolvedValue([
84+
segmentChange1,
85+
segmentChange2,
86+
segmentChange3
87+
]);
88+
89+
const segmentChangesUpdater = segmentChangesUpdaterFactory(
90+
loggerMock,
91+
mockSegmentChangesFetcher,
92+
segments,
93+
readinessManager,
94+
1000,
95+
1
96+
);
97+
98+
segments.registerSegments([segment1, segment2, segment3]);
99+
100+
// Update all segments at once
101+
await segmentChangesUpdater(undefined);
102+
103+
// Should emit once when all segments are updated
104+
expect(segmentsEmitSpy).toHaveBeenCalledTimes(1);
105+
expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
106+
});
107+
108+
test('test with empty segments - should still emit SEGMENTS_UPDATE metadata', async () => {
109+
const segmentName = 'empty-segment';
110+
const segmentChange: ISegmentChangesResponse = {
111+
name: segmentName,
112+
added: [],
113+
removed: [],
114+
since: -1,
115+
till: 123
116+
};
117+
118+
const mockSegmentChangesFetcher: ISegmentChangesFetcher = jest.fn().mockResolvedValue([segmentChange]);
119+
120+
const segmentChangesUpdater = segmentChangesUpdaterFactory(
121+
loggerMock,
122+
mockSegmentChangesFetcher,
123+
segments,
124+
readinessManager,
125+
1000,
126+
1
127+
);
128+
129+
segments.registerSegments([segmentName]);
130+
131+
await segmentChangesUpdater(undefined, segmentName);
132+
133+
expect(segmentsEmitSpy).toBeCalledWith(SDK_SEGMENTS_ARRIVED, { type: SdkUpdateMetadataKeys.SEGMENTS_UPDATE, names: [] });
134+
});
53135
});

0 commit comments

Comments
 (0)