@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
180 lines (151 loc) • 6.51 kB
text/typescript
import { objectAssign } from '../utils/lang/objectAssign';
import { ISettings } from '../types';
import SplitIO, { SdkReadyMetadata } from '../../types/splitio';
import { SDK_DEFINITIONS_ARRIVED, SDK_DEFINITIONS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants';
import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, IDefinitionsEventEmitter } from './types';
function definitionsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): IDefinitionsEventEmitter {
const definitionsEventEmitter = objectAssign(new EventEmitter(), {
definitionsArrived: false,
definitionsCacheLoaded: false,
hasInit: false,
initCallbacks: []
});
// `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)
definitionsEventEmitter.on(SDK_DEFINITIONS_ARRIVED, (metadata: SplitIO.SdkUpdateMetadata, isSplitKill: boolean) => { if (!isSplitKill) definitionsEventEmitter.definitionsArrived = true; });
definitionsEventEmitter.once(SDK_DEFINITIONS_CACHE_LOADED, () => { definitionsEventEmitter.definitionsCacheLoaded = true; });
return definitionsEventEmitter;
}
function segmentsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISegmentsEventEmitter {
const segmentsEventEmitter = objectAssign(new EventEmitter(), {
segmentsArrived: false
});
segmentsEventEmitter.once(SDK_SEGMENTS_ARRIVED, () => { segmentsEventEmitter.segmentsArrived = true; });
return segmentsEventEmitter;
}
/**
* Factory of readiness manager, which handles the ready / update event propagation.
*/
export function readinessManagerFactory(
EventEmitter: new () => SplitIO.IEventEmitter,
settings: ISettings,
definitions: IDefinitionsEventEmitter = definitionsEventEmitterFactory(EventEmitter),
isShared?: boolean
): IReadinessManager {
const readyTimeout = settings.startup.readyTimeout;
const segments: ISegmentsEventEmitter = segmentsEventEmitterFactory(EventEmitter);
const gate: IReadinessEventEmitter = new EventEmitter();
let lastUpdate = 0;
function syncLastUpdate() {
const dateNow = Date.now();
// ensure lastUpdate is always increasing per event, is case Date.now() is mocked or its value is the same
lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1;
}
let metadataReady: SdkReadyMetadata = {
initialCacheLoad: true
};
// emit SDK_READY_FROM_CACHE
let isReadyFromCache = false;
if (definitions.definitionsCacheLoaded) isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE
else definitions.once(SDK_DEFINITIONS_CACHE_LOADED, checkIsReadyFromCache);
// emit SDK_READY_TIMED_OUT
let hasTimedout = false;
function timeout() { // eslint-disable-next-line no-use-before-define
if (hasTimedout || isReady) return;
hasTimedout = true;
syncLastUpdate();
gate.emit(SDK_READY_TIMED_OUT, 'SDK client emitted SDK_READY_TIMED_OUT event.');
}
// emit SDK_READY and SDK_UPDATE
let isReady = false;
definitions.on(SDK_DEFINITIONS_ARRIVED, checkIsReadyOrUpdate);
segments.on(SDK_SEGMENTS_ARRIVED, checkIsReadyOrUpdate);
let isDestroyed = false;
let readyTimeoutId: ReturnType<typeof setTimeout>;
function __init() {
isDestroyed = false;
if (readyTimeout > 0 && !isReady) readyTimeoutId = setTimeout(timeout, readyTimeout);
}
definitions.initCallbacks.push(__init);
if (definitions.hasInit) __init();
function checkIsReadyFromCache(cacheMetadata: SdkReadyMetadata) {
metadataReady = cacheMetadata;
isReadyFromCache = true;
// Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
if (!isReady && !isDestroyed) {
try {
syncLastUpdate();
gate.emit(SDK_READY_FROM_CACHE, cacheMetadata);
} catch (e) {
// throws user callback exceptions in next tick
setTimeout(() => { throw e; }, 0);
}
}
}
function checkIsReadyOrUpdate(metadata: SplitIO.SdkUpdateMetadata) {
if (isDestroyed) return;
if (isReady) {
try {
syncLastUpdate();
gate.emit(SDK_UPDATE, metadata);
} catch (e) {
// throws user callback exceptions in next tick
setTimeout(() => { throw e; }, 0);
}
} else {
if (definitions.definitionsArrived && segments.segmentsArrived) {
clearTimeout(readyTimeoutId);
isReady = true;
try {
syncLastUpdate();
if (!isReadyFromCache) {
isReadyFromCache = true;
const metadataReadyFromCache: SplitIO.SdkReadyMetadata = {
initialCacheLoad: true, // Fresh install, no cache existed
lastUpdateTimestamp: undefined // No cache timestamp when fresh install
};
gate.emit(SDK_READY_FROM_CACHE, metadataReadyFromCache);
}
gate.emit(SDK_READY, metadataReady);
} catch (e) {
// throws user callback exceptions in next tick
setTimeout(() => { throw e; }, 0);
}
}
}
}
return {
definitions,
segments,
gate,
shared() {
return readinessManagerFactory(EventEmitter, settings, definitions, true);
},
// @TODO review/remove next methods when non-recoverable errors are reworked
// Called on consumer mode, when storage fails to connect
timeout,
// Called on 403 error (client-side SDK key on server-side), to set the SDK as destroyed for
// tracking and evaluations, while keeping event listeners to emit SDK_READY_TIMED_OUT event
setDestroyed() { isDestroyed = true; },
init() {
if (definitions.hasInit) return;
definitions.hasInit = true;
definitions.initCallbacks.forEach(cb => cb());
},
destroy() {
isDestroyed = true;
syncLastUpdate();
clearTimeout(readyTimeoutId);
if (!isShared) definitions.hasInit = false;
},
isReady() { return isReady; },
isReadyFromCache() { return isReadyFromCache; },
isTimedout() { return hasTimedout && !isReady; },
hasTimedout() { return hasTimedout; },
isDestroyed() { return isDestroyed; },
isOperational() { return (isReady || isReadyFromCache) && !isDestroyed; },
lastUpdate() { return lastUpdate; },
metadataReady() { return metadataReady; }
};
}