Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
140 changes: 140 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, SdkReadyMetadata } from '../../../types/splitio';

const settings = {
startup: {
Expand Down Expand Up @@ -300,3 +301,142 @@ test('READINESS MANAGER / Destroy before it was ready and timedout', (done) => {
}, settingsWithTimeout.startup.readyTimeout * 1.5);

});

test('READINESS MANAGER / SDK_UPDATE should emit with metadata', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

// SDK_READY
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

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

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

readinessManager.splits.emit(SDK_SPLITS_ARRIVED, metadata);

expect(receivedMetadata).toEqual(metadata);
});

test('READINESS MANAGER / SDK_UPDATE should handle undefined metadata', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

// SDK_READY
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

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

readinessManager.splits.emit(SDK_SPLITS_ARRIVED);

expect(receivedMetadata).toBeUndefined();
});

test('READINESS MANAGER / SDK_UPDATE should forward metadata from segments', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

// SDK_READY
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);

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

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

readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED, metadata);

expect(receivedMetadata).toEqual(metadata);
});

test('READINESS MANAGER / SDK_READY_FROM_CACHE should emit with metadata when cache is loaded', () => {
const readinessManager = readinessManagerFactory(EventEmitter, settings);

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);
});
25 changes: 19 additions & 6 deletions src/readiness/readinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter
// `isSplitKill` condition avoids an edge-case of wrongly emitting SDK_READY if:
// - `/memberships` fetch and SPLIT_KILL occurs before `/splitChanges` fetch, and
// - storage has cached splits (for which case `splitsStorage.killLocally` can return true)
splitsEventEmitter.on(SDK_SPLITS_ARRIVED, (isSplitKill: boolean) => { if (!isSplitKill) splitsEventEmitter.splitsArrived = true; });
splitsEventEmitter.on(SDK_SPLITS_ARRIVED, (metadata: SplitIO.SdkUpdateMetadata, isSplitKill: boolean) => { if (!isSplitKill) splitsEventEmitter.splitsArrived = true; });
splitsEventEmitter.once(SDK_SPLITS_CACHE_LOADED, () => { splitsEventEmitter.splitsCacheLoaded = true; });

return splitsEventEmitter;
Expand Down Expand Up @@ -90,20 +90,24 @@ 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(diff: any) {
function checkIsReadyOrUpdate(metadata: SplitIO.SdkUpdateMetadata) {
if (isDestroyed) return;
if (isReady) {
try {
syncLastUpdate();
gate.emit(SDK_UPDATE, diff);
gate.emit(SDK_UPDATE, metadata);
} catch (e) {
// throws user callback exceptions in next tick
setTimeout(() => { throw e; }, 0);
Expand All @@ -114,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
40 changes: 27 additions & 13 deletions src/readiness/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
import SplitIO from '../../types/splitio';

/** Readiness event types */

export type SDK_READY_TIMED_OUT = 'init::timeout'
export type SDK_READY = 'init::ready'
export type SDK_READY_FROM_CACHE = 'init::cache-ready'
export type SDK_UPDATE = 'state::update'
export type SDK_DESTROY = 'state::destroy'

export type IReadinessEvent = SDK_READY_TIMED_OUT | SDK_READY | SDK_READY_FROM_CACHE | SDK_UPDATE | SDK_DESTROY

export interface IReadinessEventEmitter extends SplitIO.IEventEmitter {
emit(event: IReadinessEvent, ...args: any[]): boolean
on(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
on(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
on(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this;
once(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
once(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
once(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
once(event: string | symbol, listener: (...args: any[]) => void): this;
addListener(event: SDK_READY, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
addListener(event: SDK_READY_FROM_CACHE, listener: (metadata: SplitIO.SdkReadyMetadata) => void): this;
addListener(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
addListener(event: string | symbol, listener: (...args: any[]) => void): this;
}
/** Splits data emitter */

type SDK_SPLITS_ARRIVED = 'state::splits-arrived'
Expand All @@ -9,6 +34,7 @@ type ISplitsEvent = SDK_SPLITS_ARRIVED | SDK_SPLITS_CACHE_LOADED
export interface ISplitsEventEmitter extends SplitIO.IEventEmitter {
emit(event: ISplitsEvent, ...args: any[]): boolean
on(event: ISplitsEvent, listener: (...args: any[]) => void): this;
on(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
once(event: ISplitsEvent, listener: (...args: any[]) => void): this;
splitsArrived: boolean
splitsCacheLoaded: boolean
Expand All @@ -24,23 +50,11 @@ type ISegmentsEvent = SDK_SEGMENTS_ARRIVED
export interface ISegmentsEventEmitter extends SplitIO.IEventEmitter {
emit(event: ISegmentsEvent, ...args: any[]): boolean
on(event: ISegmentsEvent, listener: (...args: any[]) => void): this;
on(event: SDK_UPDATE, listener: (metadata: SplitIO.SdkUpdateMetadata) => void): this;
once(event: ISegmentsEvent, listener: (...args: any[]) => void): this;
segmentsArrived: boolean
}

/** Readiness emitter */

export type SDK_READY_TIMED_OUT = 'init::timeout'
export type SDK_READY = 'init::ready'
export type SDK_READY_FROM_CACHE = 'init::cache-ready'
export type SDK_UPDATE = 'state::update'
export type SDK_DESTROY = 'state::destroy'
export type IReadinessEvent = SDK_READY_TIMED_OUT | SDK_READY | SDK_READY_FROM_CACHE | SDK_UPDATE | SDK_DESTROY

export interface IReadinessEventEmitter extends SplitIO.IEventEmitter {
emit(event: IReadinessEvent, ...args: any[]): boolean
}

/** Readiness manager */

export interface IReadinessManager {
Expand Down
Loading