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; 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; } }; }