From 1eeff81e5153f976290980c5e7bbcc7fe6146298 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 1 Oct 2025 11:39:56 -0300 Subject: [PATCH 01/12] Add method to retrieve client readiness status synchronously --- .../__tests__/sdkReadinessManager.spec.ts | 4 +- src/readiness/sdkReadinessManager.ts | 2 +- src/readiness/types.ts | 3 +- src/types.ts | 15 ------ types/splitio.d.ts | 52 +++++++++++++++++++ 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index 9044fc72..35ee9d7a 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -51,8 +51,8 @@ describe('SDK Readiness Manager - Event emitter', () => { }); expect(typeof sdkStatus.ready).toBe('function'); // The sdkStatus exposes a .ready() function. - expect(typeof sdkStatus.__getStatus).toBe('function'); // The sdkStatus exposes a .__getStatus() function. - expect(sdkStatus.__getStatus()).toEqual({ + expect(typeof sdkStatus.getStatus).toBe('function'); // The sdkStatus exposes a .getStatus() function. + expect(sdkStatus.getStatus()).toEqual({ isReady: false, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: false, lastUpdate: 0 }); diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index ee558d47..3f3de706 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -104,7 +104,7 @@ export function sdkReadinessManagerFactory( return readyPromise; }, - __getStatus() { + getStatus() { return { isReady: readinessManager.isReady(), isReadyFromCache: readinessManager.isReadyFromCache(), diff --git a/src/readiness/types.ts b/src/readiness/types.ts index df3c2603..2de99b43 100644 --- a/src/readiness/types.ts +++ b/src/readiness/types.ts @@ -1,4 +1,3 @@ -import { IStatusInterface } from '../types'; import SplitIO from '../../types/splitio'; /** Splits data emitter */ @@ -72,7 +71,7 @@ export interface IReadinessManager { export interface ISdkReadinessManager { readinessManager: IReadinessManager - sdkStatus: IStatusInterface + sdkStatus: SplitIO.IStatusInterface /** * Increment internalReadyCbCount, an offset value of SDK_READY listeners that are added/removed internally diff --git a/src/types.ts b/src/types.ts index ad3fa04c..5f6c7e39 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,21 +14,6 @@ export interface ISettings extends SplitIO.ISettings { readonly initialRolloutPlan?: RolloutPlan; } -/** - * SplitIO.IStatusInterface interface extended with private properties for internal use - */ -export interface IStatusInterface extends SplitIO.IStatusInterface { - // Expose status for internal purposes only. Not considered part of the public API, and might be updated eventually. - __getStatus(): { - isReady: boolean; - isReadyFromCache: boolean; - isTimedout: boolean; - hasTimedout: boolean; - isDestroyed: boolean; - isOperational: boolean; - lastUpdate: number; - }; -} /** * SplitIO.IBasicClient interface extended with private properties for internal use */ diff --git a/types/splitio.d.ts b/types/splitio.d.ts index eaa490f3..364c5208 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -668,6 +668,52 @@ declare namespace SplitIO { [status in ConsentStatus]: ConsentStatus; }; } + /** + * Readiness Status interface. It represents the readiness state of an SDK client. + */ + interface ReadinessStatus { + + /** + * `isReady` indicates if the client has triggered an `SDK_READY` event and + * thus is ready to evaluate with cached data synchronized with the backend. + */ + isReady: boolean; + + /** + * `isReadyFromCache` indicates if the client has triggered an `SDK_READY_FROM_CACHE` event and + * thus is ready to evaluate with cached data, although the data in cache might be stale. + */ + isReadyFromCache: boolean; + + /** + * `isTimedout` indicates if the client has triggered an `SDK_READY_TIMED_OUT` event and is not ready to evaluate. + * In other words, `isTimedout` is equivalent to `hasTimedout && !isReady`. + */ + isTimedout: boolean; + + /** + * `hasTimedout` indicates if the client has ever triggered an `SDK_READY_TIMED_OUT` event. + * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. + */ + hasTimedout: boolean; + + /** + * `isDestroyed` indicates if the client has been destroyed, i.e., `destroy` method has been called. + */ + isDestroyed: boolean; + + /** + * `isOperational` indicates if the client can evaluate feature flags. + * In this state, `getTreatment` calls will not return `CONTROL` due to the SDK being unready or destroyed. + * It's equivalent to `(isReady || isReadyFromCache) && !isDestroyed`. + */ + isOperational: boolean; + + /** + * `lastUpdate` indicates the timestamp of the most recent status event. + */ + lastUpdate: number; + } /** * Common API for entities that expose status handlers. */ @@ -676,6 +722,12 @@ declare namespace SplitIO { * Constant object containing the SDK events for you to use. */ Event: EventConsts; + /** + * Gets the readiness status. + * + * @returns The current readiness status. + */ + getStatus(): ReadinessStatus; /** * Returns a promise that resolves once the SDK has finished loading (`SDK_READY` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready after a timeout event, the `ready` method will return a resolved promise once the SDK is ready. From 6d6882273103fb306539775d620a91d839e3e0aa Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 8 Oct 2025 11:12:47 -0300 Subject: [PATCH 02/12] Use log level when logger is set --- CHANGES.txt | 5 ++++- package-lock.json | 4 ++-- package.json | 2 +- src/logger/index.ts | 2 -- .../logger/__tests__/index.spec.ts | 14 ++++---------- types/splitio.d.ts | 5 +---- 6 files changed, 12 insertions(+), 20 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 711d3ca3..2b93b7fc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,8 @@ +2.7.1 (October 8, 2025) + - Bugfix - Update `debug` option to support log levels when `logger` option is used. + 2.7.0 (October 7, 2025) - - Added support for custom loggers: added `logger` configuration option and `LoggerAPI.setLogger` method to allow the SDK to use a custom logger. + - Added support for custom loggers: added `logger` configuration option and `factory.Logger.setLogger` method to allow the SDK to use a custom logger. 2.6.0 (September 18, 2025) - Added `storage.wrapper` configuration option to allow the SDK to use a custom storage wrapper for the storage type `LOCALSTORAGE`. Default value is `window.localStorage`. diff --git a/package-lock.json b/package-lock.json index 7665ca86..356a4e93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.7.0", + "version": "2.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.7.0", + "version": "2.7.1", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index c14387e2..30ea1784 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.7.0", + "version": "2.7.1", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/logger/index.ts b/src/logger/index.ts index 09c3c7a9..903fb2a6 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -72,8 +72,6 @@ export class Logger implements ILogger { if (logger) { if (isLogger(logger)) { this.logger = logger; - // If custom logger is set, all logs are either enabled or disabled - if (this.logLevel !== LogLevelIndexes.NONE) this.setLogLevel(LogLevels.DEBUG); return; } else { this.error('Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); diff --git a/src/utils/settingsValidation/logger/__tests__/index.spec.ts b/src/utils/settingsValidation/logger/__tests__/index.spec.ts index 9b7e765a..4d667375 100644 --- a/src/utils/settingsValidation/logger/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/logger/__tests__/index.spec.ts @@ -49,19 +49,13 @@ describe('logger validators', () => { }); test.each(testTargets)('returns a logger with the provided log level if `debug` property is true or a string log level', (validateLogger) => { - expect(getLoggerLogLevel(validateLogger({ debug: true }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: true, logger: loggerMock }))).toBe('DEBUG'); expect(getLoggerLogLevel(validateLogger({ debug: 'DEBUG' }))).toBe('DEBUG'); - expect(getLoggerLogLevel(validateLogger({ debug: 'INFO' }))).toBe('INFO'); + expect(getLoggerLogLevel(validateLogger({ debug: 'INFO', logger: loggerMock }))).toBe('INFO'); expect(getLoggerLogLevel(validateLogger({ debug: 'WARN' }))).toBe('WARN'); - expect(getLoggerLogLevel(validateLogger({ debug: 'ERROR' }))).toBe('ERROR'); + expect(getLoggerLogLevel(validateLogger({ debug: 'ERROR', logger: loggerMock }))).toBe('ERROR'); expect(getLoggerLogLevel(validateLogger({ debug: 'NONE' }))).toBe('NONE'); - - // When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true) - expect(getLoggerLogLevel(validateLogger({ debug: 'DEBUG', logger: loggerMock }))).toBe('DEBUG'); - expect(getLoggerLogLevel(validateLogger({ debug: 'INFO', logger: loggerMock }))).toBe('DEBUG'); - expect(getLoggerLogLevel(validateLogger({ debug: 'WARN', logger: loggerMock }))).toBe('DEBUG'); - expect(getLoggerLogLevel(validateLogger({ debug: 'ERROR', logger: loggerMock }))).toBe('DEBUG'); - expect(getLoggerLogLevel(validateLogger({ debug: 'NONE', logger: loggerMock }))).toBe('NONE'); + expect(getLoggerLogLevel(validateLogger({ debug: false }))).toBe('NONE'); expect(consoleLogSpy).not.toBeCalled(); }); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index ebeba4df..4062e012 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -93,6 +93,7 @@ interface ISharedSettings { urls?: SplitIO.UrlSettings; /** * Custom logger object. If not provided, the SDK will use the default `console.log` method for all log levels. + * Set together with `debug` option to `true` or a log level string to enable logging. */ logger?: SplitIO.Logger; } @@ -145,8 +146,6 @@ interface IPluggableSharedSettings { * config.debug = ErrorLogger() * ``` * - * When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true), delegating log level control to the custom logger. - * * @defaultValue `false` */ debug?: boolean | SplitIO.LogLevel | SplitIO.ILogger; @@ -170,8 +169,6 @@ interface INonPluggableSharedSettings { * config.debug = 'WARN' * ``` * - * When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true), delegating log level control to the custom logger. - * * @defaultValue `false` */ debug?: boolean | SplitIO.LogLevel; From 9163c486e0356a1b76a0efdd0dcf1d4702dfd292 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Oct 2025 15:42:58 -0300 Subject: [PATCH 03/12] Fix typos --- src/sync/streaming/SSEHandler/index.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 6 +++--- src/trackers/telemetryTracker.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index f7a39c8b..4de5ed9d 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -25,7 +25,7 @@ export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter, const code = error.parsedData.code; telemetryTracker.streamingEvent(ABLY_ERROR, code); - // 401 errors due to invalid or expired token (e.g., if refresh token coudn't be executed) + // 401 errors due to invalid or expired token (e.g., if refresh token couldn't be executed) if (40140 <= code && code <= 40149) return true; // Others 4XX errors (e.g., bad request from the SDK) if (40000 <= code && code <= 49999) return false; diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 82fba1c5..74a6c82e 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -119,7 +119,7 @@ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) { if (!telemetry || !now) return; // No submitter created if telemetry cache is not defined const { settings, settings: { log, scheduler: { telemetryRefreshRate } }, splitApi, readiness, sdkReadinessManager } = params; - const startTime = timer(now); + const stopTimer = timer(now); const submitter = firstPushWindowDecorator( submitterFactory( @@ -131,12 +131,12 @@ export function telemetrySubmitterFactory(params: ISdkFactoryContextSync) { ); readiness.gate.once(SDK_READY_FROM_CACHE, () => { - telemetry.recordTimeUntilReadyFromCache(startTime()); + telemetry.recordTimeUntilReadyFromCache(stopTimer()); }); sdkReadinessManager.incInternalReadyCbCount(); readiness.gate.once(SDK_READY, () => { - telemetry.recordTimeUntilReady(startTime()); + telemetry.recordTimeUntilReady(stopTimer()); // Post config data when the SDK is ready and if the telemetry submitter was started if (submitter.isRunning()) { diff --git a/src/trackers/telemetryTracker.ts b/src/trackers/telemetryTracker.ts index 0312cc94..1a0ebc6e 100644 --- a/src/trackers/telemetryTracker.ts +++ b/src/trackers/telemetryTracker.ts @@ -11,11 +11,11 @@ export function telemetryTrackerFactory( ): ITelemetryTracker { if (telemetryCache && now) { - const startTime = timer(now); + const sessionTimer = timer(now); return { trackEval(method) { - const evalTime = timer(now); + const evalTimer = timer(now); return (label) => { switch (label) { @@ -25,20 +25,20 @@ export function telemetryTrackerFactory( case SDK_NOT_READY: // @ts-ignore ITelemetryCacheAsync doesn't implement the method if (telemetryCache.recordNonReadyUsage) telemetryCache.recordNonReadyUsage(); } - telemetryCache.recordLatency(method, evalTime()); + telemetryCache.recordLatency(method, evalTimer()); }; }, trackHttp(operation) { - const httpTime = timer(now); + const httpTimer = timer(now); return (error) => { - (telemetryCache as ITelemetryCacheSync).recordHttpLatency(operation, httpTime()); + (telemetryCache as ITelemetryCacheSync).recordHttpLatency(operation, httpTimer()); if (error && error.statusCode) (telemetryCache as ITelemetryCacheSync).recordHttpError(operation, error.statusCode); else (telemetryCache as ITelemetryCacheSync).recordSuccessfulSync(operation, Date.now()); }; }, sessionLength() { // @ts-ignore ITelemetryCacheAsync doesn't implement the method - if (telemetryCache.recordSessionLength) telemetryCache.recordSessionLength(startTime()); + if (telemetryCache.recordSessionLength) telemetryCache.recordSessionLength(sessionTimer()); }, streamingEvent(e, d) { if (e === AUTH_REJECTION) { From 1724ce89e5282c2d05fa89043ac7d83a6e8a5af2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 14 Oct 2025 15:47:48 -0300 Subject: [PATCH 04/12] Updated SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event --- CHANGES.txt | 3 +++ src/readiness/readinessManager.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 2b93b7fc..fdab3d6d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +2.8.0 (October XX, 2025) + - Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted. + 2.7.1 (October 8, 2025) - Bugfix - Update `debug` option to support log levels when `logger` option is used. diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index c69eedce..8a93d03c 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 { STORAGE_LOCALSTORAGE } from '../utils/constants'; function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter { const splitsEventEmitter = objectAssign(new EventEmitter(), { @@ -115,7 +114,7 @@ export function readinessManagerFactory( isReady = true; try { syncLastUpdate(); - if (!isReadyFromCache && settings.storage?.type === STORAGE_LOCALSTORAGE) { + if (!isReadyFromCache) { isReadyFromCache = true; gate.emit(SDK_READY_FROM_CACHE); } From 53cc6dbb7ff6249a12e7b5384e9a24785178d20b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 21 Oct 2025 11:52:00 -0300 Subject: [PATCH 05/12] feat: add whenReady and whenReadyFromCache methods to replace deprecated ready method --- src/readiness/sdkReadinessManager.ts | 30 ++++++++++++++++++++++++++- types/splitio.d.ts | 31 +++++++++++++++++++++------- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index 3f3de706..2bda4ed3 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -9,6 +9,7 @@ import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO const NEW_LISTENER_EVENT = 'newListener'; const REMOVE_LISTENER_EVENT = 'removeListener'; +const TIMEOUT_ERROR = new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.'); /** * SdkReadinessManager factory, which provides the public status API of SDK clients and manager: ready promise, readiness event emitter and constants (SDK_READY, etc). @@ -93,10 +94,11 @@ export function sdkReadinessManagerFactory( SDK_READY_TIMED_OUT, }, + // @TODO: remove in next major ready() { if (readinessManager.hasTimedout()) { if (!readinessManager.isReady()) { - return promiseWrapper(Promise.reject(new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.')), defaultOnRejected); + return promiseWrapper(Promise.reject(TIMEOUT_ERROR), defaultOnRejected); } else { return Promise.resolve(); } @@ -104,6 +106,32 @@ export function sdkReadinessManagerFactory( return readyPromise; }, + whenReady() { + return new Promise((resolve, reject) => { + if (readinessManager.isReady()) { + resolve(); + } else if (readinessManager.hasTimedout()) { + reject(TIMEOUT_ERROR); + } else { + readinessManager.gate.once(SDK_READY, resolve); + readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR)); + } + }); + }, + + whenReadyFromCache() { + return new Promise((resolve, reject) => { + if (readinessManager.isReadyFromCache()) { + resolve(); + } else if (readinessManager.hasTimedout()) { + reject(TIMEOUT_ERROR); + } else { + readinessManager.gate.once(SDK_READY_FROM_CACHE, resolve); + readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR)); + } + }); + }, + getStatus() { return { isReady: readinessManager.isReady(), diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 4a473fd8..58aca849 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -525,19 +525,19 @@ declare namespace SplitIO { */ type EventConsts = { /** - * The ready event. + * The ready event emitted once the SDK is ready to evaluate feature flags with cache synchronized with the backend. */ SDK_READY: 'init::ready'; /** - * The ready event when fired with cached data. + * The ready event emitted once the SDK is ready to evaluate feature flags with cache that could be stale. Use SDK_READY if you want to be sure the cache is in sync with the backend. */ SDK_READY_FROM_CACHE: 'init::cache-ready'; /** - * The timeout event. + * The timeout event emitted after `startup.readyTimeout` seconds if the SDK_READY event was not emitted. */ SDK_READY_TIMED_OUT: 'init::timeout'; /** - * The update event. + * The update event emitted when the SDK cache is updated with new data from the backend. */ SDK_UPDATE: 'state::update'; }; @@ -704,7 +704,7 @@ declare namespace SplitIO { /** * `isReadyFromCache` indicates if the client has triggered an `SDK_READY_FROM_CACHE` event and - * thus is ready to evaluate with cached data, although the data in cache might be stale. + * thus is ready to evaluate with cached data, although the data in cache might be stale, not synchronized with the backend. */ isReadyFromCache: boolean; @@ -728,7 +728,7 @@ declare namespace SplitIO { /** * `isOperational` indicates if the client can evaluate feature flags. * In this state, `getTreatment` calls will not return `CONTROL` due to the SDK being unready or destroyed. - * It's equivalent to `(isReady || isReadyFromCache) && !isDestroyed`. + * It's equivalent to `isReadyFromCache && !isDestroyed`. */ isOperational: boolean; @@ -752,7 +752,7 @@ declare namespace SplitIO { */ getStatus(): ReadinessStatus; /** - * Returns a promise that resolves once the SDK has finished loading (`SDK_READY` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). + * Returns a promise that resolves once the SDK has finished synchronizing with the backend (`SDK_READY` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready after a timeout event, the `ready` method will return a resolved promise once the SDK is ready. * * Caveats: the method was designed to avoid an unhandled Promise rejection if the rejection case is not handled, so that `onRejected` handler is optional when using promises. @@ -767,8 +767,23 @@ declare namespace SplitIO { * ``` * * @returns A promise that resolves once the SDK is ready or rejects if the SDK has timedout. + * @deprecated Use `whenReady` instead. */ ready(): Promise; + /** + * Returns a promise that resolves once the SDK is ready for evaluations using cached data synchronized with the backend (`SDK_READY` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). + * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready after a timeout event, the `whenReady` method will return a resolved promise once the SDK is ready. + * + * @returns A promise that resolves once the SDK is ready or rejects if the SDK has timedout. + */ + whenReady(): Promise; + /** + * Returns a promise that resolves once the SDK is ready for evaluations using cached data which might not yet be synchronized with the backend (`SDK_READY_FROM_CACHE` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). + * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready from cache after a timeout event, the `whenReadyFromCache` method will return a resolved promise once the SDK is ready from cache. + * + * @returns A promise that resolves once the SDK is ready from cache or rejects if the SDK has timedout. + */ + whenReadyFromCache(): Promise; } /** * Common definitions between clients for different environments interface. @@ -1702,7 +1717,7 @@ declare namespace SplitIO { * Wait for the SDK client to be ready before calling this method. * * ```js - * await factory.client().ready(); + * await factory.client().whenReady(); * const rolloutPlan = factory.getRolloutPlan(); * ``` * From 8b6a8d552e8381e560ed4b5e374c0a005f4891c4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 Oct 2025 12:21:23 -0300 Subject: [PATCH 06/12] refactor: simplify SDK readiness checks --- src/logger/constants.ts | 2 +- src/logger/messages/warn.ts | 2 +- .../__tests__/clientInputValidation.spec.ts | 2 +- src/sdkClient/client.ts | 6 ++-- src/sdkClient/clientInputValidation.ts | 4 +-- .../__tests__/index.asyncCache.spec.ts | 5 ++-- .../__tests__/index.syncCache.spec.ts | 3 +- src/sdkManager/index.ts | 8 ++--- .../__tests__/isOperational.spec.ts | 30 ++++++------------- src/utils/inputValidation/index.ts | 2 +- src/utils/inputValidation/isOperational.ts | 13 +++++--- src/utils/inputValidation/splitExistence.ts | 4 +-- 12 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index de1ebe58..ca331f82 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -60,7 +60,7 @@ export const SUBMITTERS_PUSH_PAGE_HIDDEN = 125; export const ENGINE_VALUE_INVALID = 200; export const ENGINE_VALUE_NO_ATTRIBUTES = 201; export const CLIENT_NO_LISTENER = 202; -export const CLIENT_NOT_READY = 203; +export const CLIENT_NOT_READY_FROM_CACHE = 203; export const SYNC_MYSEGMENTS_FETCH_RETRY = 204; export const SYNC_SPLITS_FETCH_FAILS = 205; export const STREAMING_PARSING_ERROR_FAILS = 206; diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 81cfda1a..8f87babd 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -14,7 +14,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Dropping %s after retry. Reason: %s.'], [c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s, keeping data to retry on next iteration. Reason: %s.'], // client status - [c.CLIENT_NOT_READY, '%s: the SDK is not ready, results may be incorrect%s. Make sure to wait for SDK readiness before using this method.'], + [c.CLIENT_NOT_READY_FROM_CACHE, '%s: the SDK is not ready to evaluate. Results may be incorrect%s. Make sure to wait for SDK readiness before using this method.'], [c.CLIENT_NO_LISTENER, 'No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'], // input validation [c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'], diff --git a/src/sdkClient/__tests__/clientInputValidation.spec.ts b/src/sdkClient/__tests__/clientInputValidation.spec.ts index f70845f7..452949e8 100644 --- a/src/sdkClient/__tests__/clientInputValidation.spec.ts +++ b/src/sdkClient/__tests__/clientInputValidation.spec.ts @@ -14,7 +14,7 @@ const EVALUATION_RESULT = 'on'; const client: any = createClientMock(EVALUATION_RESULT); const readinessManager: any = { - isReady: () => true, + isReadyFromCache: () => true, isDestroyed: () => false }; diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 0e526f72..2e431dc8 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -51,7 +51,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return treatment; }; - const evaluation = readinessManager.isReady() || readinessManager.isReadyFromCache() ? + const evaluation = readinessManager.isReadyFromCache() ? evaluateFeature(log, key, featureFlagName, attributes, storage) : isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected Promise.resolve(treatmentNotReady) : @@ -80,7 +80,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return treatments; }; - const evaluations = readinessManager.isReady() || readinessManager.isReadyFromCache() ? + const evaluations = readinessManager.isReadyFromCache() ? evaluateFeatures(log, key, featureFlagNames, attributes, storage) : isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected Promise.resolve(treatmentsNotReady(featureFlagNames)) : @@ -109,7 +109,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return treatments; }; - const evaluations = readinessManager.isReady() || readinessManager.isReadyFromCache() ? + const evaluations = readinessManager.isReadyFromCache() ? evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage, methodName) : isAsync ? Promise.resolve({}) : diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index 40765d41..b67025d7 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -9,7 +9,7 @@ import { validateSplits, validateTrafficType, validateIfNotDestroyed, - validateIfOperational, + validateIfReadyFromCache, validateEvaluationOptions } from '../utils/inputValidation'; import { startsWith } from '../utils/lang'; @@ -46,7 +46,7 @@ export function clientInputValidationDecorator true), + isReadyFromCache: jest.fn(() => true), isDestroyed: jest.fn(() => false) }, sdkStatus: jest.fn() @@ -77,7 +78,7 @@ describe('Manager with async cache', () => { const cache = new SplitsCachePluggable(loggerMock, keys, wrapperAdapter(loggerMock, {})); const manager = sdkManagerFactory({ mode: 'consumer_partial', log: loggerMock }, cache, sdkReadinessManagerMock); - expect(await manager.split('some_spplit')).toEqual(null); + expect(await manager.split('some_split')).toEqual(null); expect(await manager.splits()).toEqual([]); expect(await manager.names()).toEqual([]); @@ -98,7 +99,7 @@ describe('Manager with async cache', () => { const manager = sdkManagerFactory({ mode: 'consumer_partial', log: loggerMock }, {}, sdkReadinessManagerMock) as SplitIO.IAsyncManager; function validateManager() { - expect(manager.split('some_spplit')).resolves.toBe(null); + expect(manager.split('some_split')).resolves.toBe(null); expect(manager.splits()).resolves.toEqual([]); expect(manager.names()).resolves.toEqual([]); } diff --git a/src/sdkManager/__tests__/index.syncCache.spec.ts b/src/sdkManager/__tests__/index.syncCache.spec.ts index 391a053c..3437f008 100644 --- a/src/sdkManager/__tests__/index.syncCache.spec.ts +++ b/src/sdkManager/__tests__/index.syncCache.spec.ts @@ -9,6 +9,7 @@ import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; const sdkReadinessManagerMock = { readinessManager: { isReady: jest.fn(() => true), + isReadyFromCache: jest.fn(() => true), isDestroyed: jest.fn(() => false) }, sdkStatus: jest.fn() @@ -62,7 +63,7 @@ describe('Manager with sync cache (In Memory)', () => { sdkReadinessManagerMock.readinessManager.isDestroyed = () => true; function validateManager() { - expect(manager.split('some_spplit')).toBe(null); + expect(manager.split('some_split')).toBe(null); expect(manager.splits()).toEqual([]); expect(manager.names()).toEqual([]); } diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index d241b82e..5260170c 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -1,7 +1,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { thenable } from '../utils/promise/thenable'; import { find } from '../utils/lang'; -import { validateSplit, validateSplitExistence, validateIfNotDestroyed, validateIfOperational } from '../utils/inputValidation'; +import { validateSplit, validateSplitExistence, validateIfOperational } from '../utils/inputValidation'; import { ISplitsCacheAsync, ISplitsCacheSync } from '../storages/types'; import { ISdkReadinessManager } from '../readiness/types'; import { ISplit } from '../dtos/types'; @@ -66,7 +66,7 @@ export function sdkManagerFactory { @@ -28,37 +28,25 @@ describe('validateIfNotDestroyed', () => { }); }); -describe('validateIfOperational', () => { - - test('Should return true and log nothing if the SDK was ready.', () => { - const readinessManagerMock = { isReady: jest.fn(() => true) }; - - // @ts-ignore - expect(validateIfOperational(loggerMock, readinessManagerMock, 'test_method')).toBe(true); // It should return true if SDK was ready. - expect(readinessManagerMock.isReady).toBeCalledTimes(1); // It checks for readiness status using the context. - expect(loggerMock.warn).not.toBeCalled(); // But it should not log any warnings. - expect(loggerMock.error).not.toBeCalled(); // But it should not log any errors. - }); +describe('validateIfReadyFromCache', () => { test('Should return true and log nothing if the SDK was ready from cache.', () => { - const readinessManagerMock = { isReady: jest.fn(() => false), isReadyFromCache: jest.fn(() => true) }; + const readinessManagerMock = { isReadyFromCache: jest.fn(() => true) }; // @ts-ignore - expect(validateIfOperational(loggerMock, readinessManagerMock, 'test_method')).toBe(true); // It should return true if SDK was ready. - expect(readinessManagerMock.isReady).toBeCalledTimes(1); // It checks for SDK_READY status. + expect(validateIfReadyFromCache(loggerMock, readinessManagerMock, 'test_method')).toBe(true); // It should return true if SDK was ready. expect(readinessManagerMock.isReadyFromCache).toBeCalledTimes(1); // It checks for SDK_READY_FROM_CACHE status. expect(loggerMock.warn).not.toBeCalled(); // But it should not log any warnings. expect(loggerMock.error).not.toBeCalled(); // But it should not log any errors. }); - test('Should return false and log a warning if the SDK was not ready.', () => { - const readinessManagerMock = { isReady: jest.fn(() => false), isReadyFromCache: jest.fn(() => false) }; + test('Should return false and log a warning if the SDK was not ready from cache.', () => { + const readinessManagerMock = { isReadyFromCache: jest.fn(() => false) }; // @ts-ignore - expect(validateIfOperational(loggerMock, readinessManagerMock, 'test_method')).toBe(false); // It should return true if SDK was ready. - expect(readinessManagerMock.isReady).toBeCalledTimes(1); // It checks for SDK_READY status. + expect(validateIfReadyFromCache(loggerMock, readinessManagerMock, 'test_method')).toBe(false); // It should return true if SDK was ready. expect(readinessManagerMock.isReadyFromCache).toBeCalledTimes(1); // It checks for SDK_READY_FROM_CACHE status. - expect(loggerMock.warn).toBeCalledWith(CLIENT_NOT_READY, ['test_method', '']); // It should log the expected warning. + expect(loggerMock.warn).toBeCalledWith(CLIENT_NOT_READY_FROM_CACHE, ['test_method', '']); // It should log the expected warning. expect(loggerMock.error).not.toBeCalled(); // But it should not log any errors. }); }); diff --git a/src/utils/inputValidation/index.ts b/src/utils/inputValidation/index.ts index eac9777d..f6e06c5e 100644 --- a/src/utils/inputValidation/index.ts +++ b/src/utils/inputValidation/index.ts @@ -7,7 +7,7 @@ export { validateKey } from './key'; export { validateSplit } from './split'; export { validateSplits } from './splits'; export { validateTrafficType } from './trafficType'; -export { validateIfNotDestroyed, validateIfOperational } from './isOperational'; +export { validateIfNotDestroyed, validateIfReadyFromCache, validateIfOperational } from './isOperational'; export { validateSplitExistence } from './splitExistence'; export { validateTrafficTypeExistence } from './trafficTypeExistence'; export { validateEvaluationOptions } from './eventProperties'; diff --git a/src/utils/inputValidation/isOperational.ts b/src/utils/inputValidation/isOperational.ts index 3d990433..5f122926 100644 --- a/src/utils/inputValidation/isOperational.ts +++ b/src/utils/inputValidation/isOperational.ts @@ -1,4 +1,4 @@ -import { ERROR_CLIENT_DESTROYED, CLIENT_NOT_READY } from '../../logger/constants'; +import { ERROR_CLIENT_DESTROYED, CLIENT_NOT_READY_FROM_CACHE } from '../../logger/constants'; import { ILogger } from '../../logger/types'; import { IReadinessManager } from '../../readiness/types'; @@ -9,9 +9,14 @@ export function validateIfNotDestroyed(log: ILogger, readinessManager: IReadines return false; } -export function validateIfOperational(log: ILogger, readinessManager: IReadinessManager, method: string, featureFlagNameOrNames?: string | string[] | false) { - if (readinessManager.isReady() || readinessManager.isReadyFromCache()) return true; +export function validateIfReadyFromCache(log: ILogger, readinessManager: IReadinessManager, method: string, featureFlagNameOrNames?: string | string[] | false) { + if (readinessManager.isReadyFromCache()) return true; - log.warn(CLIENT_NOT_READY, [method, featureFlagNameOrNames ? ` for feature flag ${featureFlagNameOrNames.toString()}` : '']); + log.warn(CLIENT_NOT_READY_FROM_CACHE, [method, featureFlagNameOrNames ? ` for feature flag ${featureFlagNameOrNames.toString()}` : '']); return false; } + +// Operational means that the SDK is ready to evaluate (not destroyed and ready from cache) +export function validateIfOperational(log: ILogger, readinessManager: IReadinessManager, method: string, featureFlagNameOrNames?: string | string[] | false) { + return validateIfNotDestroyed(log, readinessManager, method) && validateIfReadyFromCache(log, readinessManager, method, featureFlagNameOrNames); +} diff --git a/src/utils/inputValidation/splitExistence.ts b/src/utils/inputValidation/splitExistence.ts index 2f3105f9..60ac3743 100644 --- a/src/utils/inputValidation/splitExistence.ts +++ b/src/utils/inputValidation/splitExistence.ts @@ -5,10 +5,10 @@ import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants'; /** * This is defined here and in this format mostly because of the logger and the fact that it's considered a validation at product level. - * But it's not going to run on the input validation layer. In any case, the most compeling reason to use it as we do is to avoid going to Redis and get a split twice. + * But it's not going to run on the input validation layer. In any case, the most compelling reason to use it as we do is to avoid going to Redis and get a split twice. */ export function validateSplitExistence(log: ILogger, readinessManager: IReadinessManager, splitName: string, labelOrSplitObj: any, method: string): boolean { - if (readinessManager.isReady()) { // Only if it's ready we validate this, otherwise it may just be that the SDK is not ready yet. + if (readinessManager.isReady()) { // Only if it's ready (synced with BE) we validate this, otherwise it may just be that the SDK is still syncing if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null) { log.warn(WARN_NOT_EXISTENT_SPLIT, [method, splitName]); return false; From 13eaec3e8026397d116e65a25b919aa606ef33be Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 Oct 2025 12:40:13 -0300 Subject: [PATCH 07/12] feat: update whenReadyFromCache to return boolean indicating SDK ready state --- src/readiness/sdkReadinessManager.ts | 6 +++--- types/splitio.d.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index 2bda4ed3..03afd873 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -120,13 +120,13 @@ export function sdkReadinessManagerFactory( }, whenReadyFromCache() { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if (readinessManager.isReadyFromCache()) { - resolve(); + resolve(readinessManager.isReady()); } else if (readinessManager.hasTimedout()) { reject(TIMEOUT_ERROR); } else { - readinessManager.gate.once(SDK_READY_FROM_CACHE, resolve); + readinessManager.gate.once(SDK_READY_FROM_CACHE, () => resolve(readinessManager.isReady())); readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR)); } }); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 58aca849..f3981076 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -781,9 +781,9 @@ declare namespace SplitIO { * Returns a promise that resolves once the SDK is ready for evaluations using cached data which might not yet be synchronized with the backend (`SDK_READY_FROM_CACHE` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready from cache after a timeout event, the `whenReadyFromCache` method will return a resolved promise once the SDK is ready from cache. * - * @returns A promise that resolves once the SDK is ready from cache or rejects if the SDK has timedout. + * @returns A promise that resolves once the SDK is ready from cache or rejects if the SDK has timedout. The promise resolves with a boolean value that indicates whether the SDK is ready (synchronized with the backend) or not. */ - whenReadyFromCache(): Promise; + whenReadyFromCache(): Promise; } /** * Common definitions between clients for different environments interface. From e2179d7f2da0407ea20bc70140da21d55c414ebb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 Oct 2025 15:32:39 -0300 Subject: [PATCH 08/12] Polishing --- src/readiness/readinessManager.ts | 4 ++-- types/splitio.d.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 8a93d03c..319e843d 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -90,7 +90,7 @@ export function readinessManagerFactory( if (!isReady && !isDestroyed) { try { syncLastUpdate(); - gate.emit(SDK_READY_FROM_CACHE); + gate.emit(SDK_READY_FROM_CACHE, isReady); } catch (e) { // throws user callback exceptions in next tick setTimeout(() => { throw e; }, 0); @@ -116,7 +116,7 @@ export function readinessManagerFactory( syncLastUpdate(); if (!isReadyFromCache) { isReadyFromCache = true; - gate.emit(SDK_READY_FROM_CACHE); + gate.emit(SDK_READY_FROM_CACHE, isReady); } gate.emit(SDK_READY); } catch (e) { diff --git a/types/splitio.d.ts b/types/splitio.d.ts index f3981076..b3884694 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -772,16 +772,17 @@ declare namespace SplitIO { ready(): Promise; /** * Returns a promise that resolves once the SDK is ready for evaluations using cached data synchronized with the backend (`SDK_READY` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). - * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready after a timeout event, the `whenReady` method will return a resolved promise once the SDK is ready. + * As it's meant to provide similar flexibility than event listeners, given that the SDK might be ready after a timeout event, the `whenReady` method will return a resolved promise once the SDK is ready. * - * @returns A promise that resolves once the SDK is ready or rejects if the SDK has timedout. + * @returns A promise that resolves once the SDK_READY event is emitted or rejects if the SDK has timedout. */ whenReady(): Promise; /** * Returns a promise that resolves once the SDK is ready for evaluations using cached data which might not yet be synchronized with the backend (`SDK_READY_FROM_CACHE` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). - * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready from cache after a timeout event, the `whenReadyFromCache` method will return a resolved promise once the SDK is ready from cache. + * As it's meant to provide similar flexibility than event listeners, given that the SDK might be ready from cache after a timeout event, the `whenReadyFromCache` method will return a resolved promise once the SDK is ready from cache. * - * @returns A promise that resolves once the SDK is ready from cache or rejects if the SDK has timedout. The promise resolves with a boolean value that indicates whether the SDK is ready (synchronized with the backend) or not. + * @returns A promise that resolves once the SDK_READY_FROM_CACHE event is emitted or rejects if the SDK has timedout. The promise resolves with a boolean value that + * indicates whether the SDK_READY_FROM_CACHE event was emitted together with the SDK_READY event (i.e., the SDK is ready and synchronized with the backend) or not. */ whenReadyFromCache(): Promise; } From e49de68dda535708d7c9775514f998b3560ce2f7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 Oct 2025 16:44:46 -0300 Subject: [PATCH 09/12] Revert "Add method to retrieve client readiness status synchronously" This reverts commit 1eeff81e5153f976290980c5e7bbcc7fe6146298. --- .../__tests__/sdkReadinessManager.spec.ts | 4 +- src/readiness/sdkReadinessManager.ts | 2 +- src/readiness/types.ts | 3 +- src/types.ts | 15 ++++++ types/splitio.d.ts | 52 ------------------- 5 files changed, 20 insertions(+), 56 deletions(-) diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index 35ee9d7a..9044fc72 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -51,8 +51,8 @@ describe('SDK Readiness Manager - Event emitter', () => { }); expect(typeof sdkStatus.ready).toBe('function'); // The sdkStatus exposes a .ready() function. - expect(typeof sdkStatus.getStatus).toBe('function'); // The sdkStatus exposes a .getStatus() function. - expect(sdkStatus.getStatus()).toEqual({ + expect(typeof sdkStatus.__getStatus).toBe('function'); // The sdkStatus exposes a .__getStatus() function. + expect(sdkStatus.__getStatus()).toEqual({ isReady: false, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: false, lastUpdate: 0 }); diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index 03afd873..62f51571 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -132,7 +132,7 @@ export function sdkReadinessManagerFactory( }); }, - getStatus() { + __getStatus() { return { isReady: readinessManager.isReady(), isReadyFromCache: readinessManager.isReadyFromCache(), diff --git a/src/readiness/types.ts b/src/readiness/types.ts index 2de99b43..df3c2603 100644 --- a/src/readiness/types.ts +++ b/src/readiness/types.ts @@ -1,3 +1,4 @@ +import { IStatusInterface } from '../types'; import SplitIO from '../../types/splitio'; /** Splits data emitter */ @@ -71,7 +72,7 @@ export interface IReadinessManager { export interface ISdkReadinessManager { readinessManager: IReadinessManager - sdkStatus: SplitIO.IStatusInterface + sdkStatus: IStatusInterface /** * Increment internalReadyCbCount, an offset value of SDK_READY listeners that are added/removed internally diff --git a/src/types.ts b/src/types.ts index 5f6c7e39..ad3fa04c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,21 @@ export interface ISettings extends SplitIO.ISettings { readonly initialRolloutPlan?: RolloutPlan; } +/** + * SplitIO.IStatusInterface interface extended with private properties for internal use + */ +export interface IStatusInterface extends SplitIO.IStatusInterface { + // Expose status for internal purposes only. Not considered part of the public API, and might be updated eventually. + __getStatus(): { + isReady: boolean; + isReadyFromCache: boolean; + isTimedout: boolean; + hasTimedout: boolean; + isDestroyed: boolean; + isOperational: boolean; + lastUpdate: number; + }; +} /** * SplitIO.IBasicClient interface extended with private properties for internal use */ diff --git a/types/splitio.d.ts b/types/splitio.d.ts index b3884694..0a8dfda2 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -691,52 +691,6 @@ declare namespace SplitIO { [status in ConsentStatus]: ConsentStatus; }; } - /** - * Readiness Status interface. It represents the readiness state of an SDK client. - */ - interface ReadinessStatus { - - /** - * `isReady` indicates if the client has triggered an `SDK_READY` event and - * thus is ready to evaluate with cached data synchronized with the backend. - */ - isReady: boolean; - - /** - * `isReadyFromCache` indicates if the client has triggered an `SDK_READY_FROM_CACHE` event and - * thus is ready to evaluate with cached data, although the data in cache might be stale, not synchronized with the backend. - */ - isReadyFromCache: boolean; - - /** - * `isTimedout` indicates if the client has triggered an `SDK_READY_TIMED_OUT` event and is not ready to evaluate. - * In other words, `isTimedout` is equivalent to `hasTimedout && !isReady`. - */ - isTimedout: boolean; - - /** - * `hasTimedout` indicates if the client has ever triggered an `SDK_READY_TIMED_OUT` event. - * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. - */ - hasTimedout: boolean; - - /** - * `isDestroyed` indicates if the client has been destroyed, i.e., `destroy` method has been called. - */ - isDestroyed: boolean; - - /** - * `isOperational` indicates if the client can evaluate feature flags. - * In this state, `getTreatment` calls will not return `CONTROL` due to the SDK being unready or destroyed. - * It's equivalent to `isReadyFromCache && !isDestroyed`. - */ - isOperational: boolean; - - /** - * `lastUpdate` indicates the timestamp of the most recent status event. - */ - lastUpdate: number; - } /** * Common API for entities that expose status handlers. */ @@ -745,12 +699,6 @@ declare namespace SplitIO { * Constant object containing the SDK events for you to use. */ Event: EventConsts; - /** - * Gets the readiness status. - * - * @returns The current readiness status. - */ - getStatus(): ReadinessStatus; /** * Returns a promise that resolves once the SDK has finished synchronizing with the backend (`SDK_READY` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready after a timeout event, the `ready` method will return a resolved promise once the SDK is ready. From 36ca35e1d0b1aceb2407adfdc5e7cfb0e4a992d2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 22 Oct 2025 17:17:05 -0300 Subject: [PATCH 10/12] Polishing --- CHANGES.txt | 1 + types/splitio.d.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index fdab3d6d..8d931493 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 2.8.0 (October XX, 2025) + - Added `client.whenReady()` and `client.whenReadyFromCache()` methods to replace the deprecated `client.ready()` method, which had an issue causing the returned promise to hang when using async/await syntax if it was rejected. - Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted. 2.7.1 (October 8, 2025) diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 0a8dfda2..49f70c62 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -700,7 +700,7 @@ declare namespace SplitIO { */ Event: EventConsts; /** - * Returns a promise that resolves once the SDK has finished synchronizing with the backend (`SDK_READY` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). + * Returns a promise that resolves when the SDK has finished initial synchronization with the backend (`SDK_READY` event emitted), or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). * As it's meant to provide similar flexibility to the event approach, given that the SDK might be eventually ready after a timeout event, the `ready` method will return a resolved promise once the SDK is ready. * * Caveats: the method was designed to avoid an unhandled Promise rejection if the rejection case is not handled, so that `onRejected` handler is optional when using promises. @@ -719,15 +719,17 @@ declare namespace SplitIO { */ ready(): Promise; /** - * Returns a promise that resolves once the SDK is ready for evaluations using cached data synchronized with the backend (`SDK_READY` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). + * Returns a promise that resolves when the SDK has finished initial synchronization with the backend (`SDK_READY` event emitted), or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). * As it's meant to provide similar flexibility than event listeners, given that the SDK might be ready after a timeout event, the `whenReady` method will return a resolved promise once the SDK is ready. + * You must handle the promise rejection to avoid an unhandled promise rejection error, or set the `startup.readyTimeout` configuration option to 0 to avoid the timeout and thus the rejection. * * @returns A promise that resolves once the SDK_READY event is emitted or rejects if the SDK has timedout. */ whenReady(): Promise; /** - * Returns a promise that resolves once the SDK is ready for evaluations using cached data which might not yet be synchronized with the backend (`SDK_READY_FROM_CACHE` event emitted) or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). + * Returns a promise that resolves when the SDK is ready for evaluations using cached data, which might not yet be synchronized with the backend (`SDK_READY_FROM_CACHE` event emitted), or rejected if the SDK has timedout (`SDK_READY_TIMED_OUT` event emitted). * As it's meant to provide similar flexibility than event listeners, given that the SDK might be ready from cache after a timeout event, the `whenReadyFromCache` method will return a resolved promise once the SDK is ready from cache. + * You must handle the promise rejection to avoid an unhandled promise rejection error, or set the `startup.readyTimeout` configuration option to 0 to avoid the timeout and thus the rejection. * * @returns A promise that resolves once the SDK_READY_FROM_CACHE event is emitted or rejects if the SDK has timedout. The promise resolves with a boolean value that * indicates whether the SDK_READY_FROM_CACHE event was emitted together with the SDK_READY event (i.e., the SDK is ready and synchronized with the backend) or not. From b8719b299df104bc2759861f22cb78b91958aec1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 23 Oct 2025 11:20:33 -0300 Subject: [PATCH 11/12] Polishing --- CHANGES.txt | 2 +- .../__tests__/sdkReadinessManager.spec.ts | 72 +++++++++++++------ src/readiness/sdkReadinessManager.ts | 4 +- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8d931493..9163d52f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ 2.8.0 (October XX, 2025) - - Added `client.whenReady()` and `client.whenReadyFromCache()` methods to replace the deprecated `client.ready()` method, which had an issue causing the returned promise to hang when using async/await syntax if it was rejected. + - Added `client.whenReady()` and `client.whenReadyFromCache()` methods to replace the deprecated `client.ready()` method, which has an issue causing the returned promise to hang when using async/await syntax if it was rejected. - Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted. 2.7.1 (October 8, 2025) diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index 9044fc72..d4de5124 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -1,11 +1,12 @@ // @ts-nocheck import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import SplitIO from '../../../types/splitio'; -import { SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT, SDK_UPDATE } from '../constants'; +import { SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../constants'; import { sdkReadinessManagerFactory } from '../sdkReadinessManager'; import { IReadinessManager } from '../types'; import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO_LISTENER } from '../../logger/constants'; import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; +import { EventEmitter } from '../../utils/MinEvents'; const EventEmitterMock = jest.fn(() => ({ on: jest.fn(), @@ -24,6 +25,7 @@ function emitReadyEvent(readinessManager: IReadinessManager) { readinessManager.segments.once.mock.calls[0][1](); readinessManager.segments.on.mock.calls[0][1](); readinessManager.gate.once.mock.calls[0][1](); + if (readinessManager.gate.once.mock.calls[3]) readinessManager.gate.once.mock.calls[3][1](); // whenReady promise } const timeoutErrorMessage = 'Split SDK emitted SDK_READY_TIMED_OUT event.'; @@ -32,6 +34,7 @@ const timeoutErrorMessage = 'Split SDK emitted SDK_READY_TIMED_OUT event.'; function emitTimeoutEvent(readinessManager: IReadinessManager) { readinessManager.gate.once.mock.calls[1][1](timeoutErrorMessage); readinessManager.hasTimedout = () => true; + if (readinessManager.gate.once.mock.calls[4]) readinessManager.gate.once.mock.calls[4][1](timeoutErrorMessage); // whenReady promise } describe('SDK Readiness Manager - Event emitter', () => { @@ -50,7 +53,8 @@ describe('SDK Readiness Manager - Event emitter', () => { expect(sdkStatus[propName]).toBeTruthy(); // The sdkStatus exposes all minimal EventEmitter functionality. }); - expect(typeof sdkStatus.ready).toBe('function'); // The sdkStatus exposes a .ready() function. + expect(typeof sdkStatus.whenReady).toBe('function'); // The sdkStatus exposes a .whenReady() function. + expect(typeof sdkStatus.whenReadyFromCache).toBe('function'); // The sdkStatus exposes a .whenReadyFromCache() function. expect(typeof sdkStatus.__getStatus).toBe('function'); // The sdkStatus exposes a .__getStatus() function. expect(sdkStatus.__getStatus()).toEqual({ isReady: false, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: false, lastUpdate: 0 @@ -67,9 +71,9 @@ describe('SDK Readiness Manager - Event emitter', () => { const sdkReadyResolvePromiseCall = gateMock.once.mock.calls[0]; const sdkReadyRejectPromiseCall = gateMock.once.mock.calls[1]; const sdkReadyFromCacheListenersCheckCall = gateMock.once.mock.calls[2]; - expect(sdkReadyResolvePromiseCall[0]).toBe(SDK_READY); // A one time only subscription is on the SDK_READY event, for resolving the full blown ready promise and to check for callbacks warning. - expect(sdkReadyRejectPromiseCall[0]).toBe(SDK_READY_TIMED_OUT); // A one time only subscription is also on the SDK_READY_TIMED_OUT event, for rejecting the full blown ready promise. - expect(sdkReadyFromCacheListenersCheckCall[0]).toBe(SDK_READY_FROM_CACHE); // A one time only subscription is on the SDK_READY_FROM_CACHE event, to log the event and update internal state. + expect(sdkReadyResolvePromiseCall[0]).toBe(SDK_READY); // A one time only subscription is on the SDK_READY event + expect(sdkReadyRejectPromiseCall[0]).toBe(SDK_READY_TIMED_OUT); // A one time only subscription is also on the SDK_READY_TIMED_OUT event + expect(sdkReadyFromCacheListenersCheckCall[0]).toBe(SDK_READY_FROM_CACHE); // A one time only subscription is on the SDK_READY_FROM_CACHE event expect(gateMock.on).toBeCalledTimes(2); // It should also add two persistent listeners @@ -98,7 +102,7 @@ describe('SDK Readiness Manager - Event emitter', () => { emitReadyEvent(sdkReadinessManager.readinessManager); - expect(loggerMock.warn).toBeCalledTimes(1); // If the SDK_READY event fires and we have no callbacks for it (neither event nor ready promise) we get a warning. + expect(loggerMock.warn).toBeCalledTimes(1); // If the SDK_READY event fires and we have no callbacks for it (neither event nor whenReady promise) we get a warning. expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // Telling us there were no listeners and evaluations before this point may have been incorrect. expect(loggerMock.info).toBeCalledTimes(1); // If the SDK_READY event fires, we get a info message. @@ -199,15 +203,15 @@ describe('SDK Readiness Manager - Event emitter', () => { }); }); -describe('SDK Readiness Manager - Ready promise', () => { +describe('SDK Readiness Manager - whenReady promise', () => { - test('.ready() promise behavior for clients', async () => { + test('.whenReady() promise behavior for clients', async () => { const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings); - const ready = sdkReadinessManager.sdkStatus.ready(); + const ready = sdkReadinessManager.sdkStatus.whenReady(); expect(ready instanceof Promise).toBe(true); // It should return a promise. - // make the SDK "ready" + // make the SDK ready emitReadyEvent(sdkReadinessManager.readinessManager); let testPassedCount = 0; @@ -219,8 +223,8 @@ describe('SDK Readiness Manager - Ready promise', () => { () => { throw new Error('It should be resolved on ready event, not rejected.'); } ); - // any subsequent call to .ready() must be a resolved promise - await ready.then( + // any subsequent call to .whenReady() must be a resolved promise + await sdkReadinessManager.sdkStatus.whenReady().then( () => { expect('A subsequent call should be a resolved promise.'); testPassedCount++; @@ -233,9 +237,9 @@ describe('SDK Readiness Manager - Ready promise', () => { const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(EventEmitterMock, fullSettings); - const readyForTimeout = sdkReadinessManagerForTimedout.sdkStatus.ready(); + const readyForTimeout = sdkReadinessManagerForTimedout.sdkStatus.whenReady(); - emitTimeoutEvent(sdkReadinessManagerForTimedout.readinessManager); // make the SDK "timed out" + emitTimeoutEvent(sdkReadinessManagerForTimedout.readinessManager); // make the SDK timeout await readyForTimeout.then( () => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); }, @@ -245,8 +249,8 @@ describe('SDK Readiness Manager - Ready promise', () => { } ); - // any subsequent call to .ready() must be a rejected promise - await readyForTimeout.then( + // any subsequent call to .whenReady() must be a rejected promise until the SDK is ready + await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then( () => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); }, () => { expect('A subsequent call should be a rejected promise.'); @@ -254,11 +258,11 @@ describe('SDK Readiness Manager - Ready promise', () => { } ); - // make the SDK "ready" + // make the SDK ready emitReadyEvent(sdkReadinessManagerForTimedout.readinessManager); - // once SDK_READY, `.ready()` returns a resolved promise - await ready.then( + // once SDK_READY, `.whenReady()` returns a resolved promise + await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then( () => { expect('It should be a resolved promise when the SDK is ready, even after an SDK timeout.'); loggerMock.mockClear(); @@ -269,7 +273,35 @@ describe('SDK Readiness Manager - Ready promise', () => { ); }); - test('Full blown ready promise count as a callback and resolves on SDK_READY', (done) => { + test('whenReady promise count as a callback and resolves on SDK_READY', (done) => { + let sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings); + + // Emit ready event + sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + sdkReadinessManager.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + + expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // We should get a warning if the SDK get's ready before calling the whenReady method or attaching a listener to the ready event + loggerMock.warn.mockClear(); + + sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings); + sdkReadinessManager.sdkStatus.whenReady().then(() => { + expect('whenReady promise is resolved when the gate emits SDK_READY.'); + done(); + }, () => { + throw new Error('This should not be called as the promise is being resolved.'); + }); + + // Emit ready event + sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + sdkReadinessManager.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + + expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener or call the whenReady method, we get no warnings. + }); +}); + +// @TODO: remove in next major +describe('SDK Readiness Manager - Ready promise', () => { + test('ready promise count as a callback and resolves on SDK_READY', (done) => { const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings); const readyPromise = sdkReadinessManager.sdkStatus.ready(); diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index 62f51571..857a9e84 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -9,7 +9,7 @@ import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO const NEW_LISTENER_EVENT = 'newListener'; const REMOVE_LISTENER_EVENT = 'removeListener'; -const TIMEOUT_ERROR = new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.'); +const TIMEOUT_ERROR = new Error(SDK_READY_TIMED_OUT); /** * SdkReadinessManager factory, which provides the public status API of SDK clients and manager: ready promise, readiness event emitter and constants (SDK_READY, etc). @@ -98,7 +98,7 @@ export function sdkReadinessManagerFactory( ready() { if (readinessManager.hasTimedout()) { if (!readinessManager.isReady()) { - return promiseWrapper(Promise.reject(TIMEOUT_ERROR), defaultOnRejected); + return promiseWrapper(Promise.reject(new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.')), defaultOnRejected); } else { return Promise.resolve(); } From 94814df1f6630c28154778a5b58e6b630e4b28cd Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 23 Oct 2025 15:46:26 -0300 Subject: [PATCH 12/12] Update logs and tests --- src/logger/messages/warn.ts | 2 +- .../__tests__/sdkReadinessManager.spec.ts | 114 +++++++++--------- src/readiness/sdkReadinessManager.ts | 2 + 3 files changed, 62 insertions(+), 56 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 8f87babd..a0bf31a9 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -15,7 +15,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s, keeping data to retry on next iteration. Reason: %s.'], // client status [c.CLIENT_NOT_READY_FROM_CACHE, '%s: the SDK is not ready to evaluate. Results may be incorrect%s. Make sure to wait for SDK readiness before using this method.'], - [c.CLIENT_NO_LISTENER, 'No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'], + [c.CLIENT_NO_LISTENER, 'No listeners for SDK_READY event detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet synchronized with the backend.'], // input validation [c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'], [c.WARN_TRIMMING_PROPERTIES, '%s: more than 300 properties were provided. Some of them will be trimmed when processed.'], diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index d4de5124..4d47d12f 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -1,7 +1,7 @@ // @ts-nocheck import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; import SplitIO from '../../../types/splitio'; -import { SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../constants'; +import { SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT, SDK_UPDATE, SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../constants'; import { sdkReadinessManagerFactory } from '../sdkReadinessManager'; import { IReadinessManager } from '../types'; import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO_LISTENER } from '../../logger/constants'; @@ -20,6 +20,12 @@ const EventEmitterMock = jest.fn(() => ({ // Makes readinessManager emit SDK_READY & update isReady flag function emitReadyEvent(readinessManager: IReadinessManager) { + if (readinessManager.gate instanceof EventEmitter) { + readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + return; + } + readinessManager.splits.once.mock.calls[0][1](); readinessManager.splits.on.mock.calls[0][1](); readinessManager.segments.once.mock.calls[0][1](); @@ -32,6 +38,11 @@ const timeoutErrorMessage = 'Split SDK emitted SDK_READY_TIMED_OUT event.'; // Makes readinessManager emit SDK_READY_TIMED_OUT & update hasTimedout flag function emitTimeoutEvent(readinessManager: IReadinessManager) { + if (readinessManager.gate instanceof EventEmitter) { + readinessManager.timeout(); + return; + } + readinessManager.gate.once.mock.calls[1][1](timeoutErrorMessage); readinessManager.hasTimedout = () => true; if (readinessManager.gate.once.mock.calls[4]) readinessManager.gate.once.mock.calls[4][1](timeoutErrorMessage); // whenReady promise @@ -39,7 +50,7 @@ function emitTimeoutEvent(readinessManager: IReadinessManager) { describe('SDK Readiness Manager - Event emitter', () => { - afterEach(() => { loggerMock.mockClear(); }); + beforeEach(() => { loggerMock.mockClear(); }); test('Providing the gate object to get the SDK status interface that manages events', () => { expect(typeof sdkReadinessManagerFactory).toBe('function'); // The module exposes a function. @@ -203,82 +214,74 @@ describe('SDK Readiness Manager - Event emitter', () => { }); }); -describe('SDK Readiness Manager - whenReady promise', () => { +describe('SDK Readiness Manager - Promises', () => { - test('.whenReady() promise behavior for clients', async () => { - const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings); + test('.whenReady() and .whenReadyFromCache() promises resolves when SDK_READY is emitted', async () => { + const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings); + + // make the SDK ready from cache + sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED); + expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(false); + + // validate error log for SDK_READY_FROM_CACHE + expect(loggerMock.error).not.toBeCalled(); + sdkReadinessManager.readinessManager.gate.on(SDK_READY_FROM_CACHE, () => {}); + expect(loggerMock.error).toBeCalledWith(ERROR_CLIENT_LISTENER, ['SDK_READY_FROM_CACHE']); + const readyFromCache = sdkReadinessManager.sdkStatus.whenReadyFromCache(); const ready = sdkReadinessManager.sdkStatus.whenReady(); - expect(ready instanceof Promise).toBe(true); // It should return a promise. // make the SDK ready emitReadyEvent(sdkReadinessManager.readinessManager); + expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(true); let testPassedCount = 0; - await ready.then( - () => { - expect('It should be a promise that will be resolved when the SDK is ready.'); - testPassedCount++; - }, - () => { throw new Error('It should be resolved on ready event, not rejected.'); } - ); + function incTestPassedCount() { testPassedCount++; } + function throwTestFailed() { throw new Error('It should be resolved, not rejected.'); } - // any subsequent call to .whenReady() must be a resolved promise - await sdkReadinessManager.sdkStatus.whenReady().then( - () => { - expect('A subsequent call should be a resolved promise.'); - testPassedCount++; - }, - () => { throw new Error('It should be resolved on ready event, not rejected.'); } - ); + await readyFromCache.then(incTestPassedCount, throwTestFailed); + await ready.then(incTestPassedCount, throwTestFailed); - // control assertion. stubs already reset. - expect(testPassedCount).toBe(2); + // any subsequent call to .whenReady() and .whenReadyFromCache() must be a resolved promise + await sdkReadinessManager.sdkStatus.whenReady().then(incTestPassedCount, throwTestFailed); + await sdkReadinessManager.sdkStatus.whenReadyFromCache().then(incTestPassedCount, throwTestFailed); - const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(EventEmitterMock, fullSettings); + expect(testPassedCount).toBe(4); + }); + test('.whenReady() and .whenReadyFromCache() promises reject when SDK_READY_TIMED_OUT is emitted before SDK_READY', async () => { + const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(EventEmitter, fullSettings); + + const readyFromCacheForTimeout = sdkReadinessManagerForTimedout.sdkStatus.whenReadyFromCache(); const readyForTimeout = sdkReadinessManagerForTimedout.sdkStatus.whenReady(); emitTimeoutEvent(sdkReadinessManagerForTimedout.readinessManager); // make the SDK timeout - await readyForTimeout.then( - () => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); }, - () => { - expect('It should be a promise that will be rejected when the SDK is timed out.'); - testPassedCount++; - } - ); + let testPassedCount = 0; + function incTestPassedCount() { testPassedCount++; } + function throwTestFailed() { throw new Error('It should rejected, not resolved.'); } - // any subsequent call to .whenReady() must be a rejected promise until the SDK is ready - await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then( - () => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); }, - () => { - expect('A subsequent call should be a rejected promise.'); - testPassedCount++; - } - ); + await readyFromCacheForTimeout.then(throwTestFailed,incTestPassedCount); + await readyForTimeout.then(throwTestFailed,incTestPassedCount); + + // any subsequent call to .whenReady() and .whenReadyFromCache() must be a rejected promise until the SDK is ready + await sdkReadinessManagerForTimedout.sdkStatus.whenReadyFromCache().then(throwTestFailed,incTestPassedCount); + await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then(throwTestFailed,incTestPassedCount); // make the SDK ready emitReadyEvent(sdkReadinessManagerForTimedout.readinessManager); // once SDK_READY, `.whenReady()` returns a resolved promise - await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then( - () => { - expect('It should be a resolved promise when the SDK is ready, even after an SDK timeout.'); - loggerMock.mockClear(); - testPassedCount++; - expect(testPassedCount).toBe(5); - }, - () => { throw new Error('It should be resolved on ready event, not rejected.'); } - ); + await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then(incTestPassedCount, throwTestFailed); + await sdkReadinessManagerForTimedout.sdkStatus.whenReadyFromCache().then(incTestPassedCount, throwTestFailed); + + expect(testPassedCount).toBe(6); }); - test('whenReady promise count as a callback and resolves on SDK_READY', (done) => { + test('whenReady promise counts as an SDK_READY listener', (done) => { let sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings); - // Emit ready event - sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - sdkReadinessManager.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + emitReadyEvent(sdkReadinessManager.readinessManager); expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // We should get a warning if the SDK get's ready before calling the whenReady method or attaching a listener to the ready event loggerMock.warn.mockClear(); @@ -291,9 +294,7 @@ describe('SDK Readiness Manager - whenReady promise', () => { throw new Error('This should not be called as the promise is being resolved.'); }); - // Emit ready event - sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - sdkReadinessManager.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + emitReadyEvent(sdkReadinessManager.readinessManager); expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener or call the whenReady method, we get no warnings. }); @@ -301,6 +302,9 @@ describe('SDK Readiness Manager - whenReady promise', () => { // @TODO: remove in next major describe('SDK Readiness Manager - Ready promise', () => { + + beforeEach(() => { loggerMock.mockClear(); }); + test('ready promise count as a callback and resolves on SDK_READY', (done) => { const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings); const readyPromise = sdkReadinessManager.sdkStatus.ready(); diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index 857a9e84..64e518b3 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -39,6 +39,8 @@ export function sdkReadinessManagerFactory( } else if (event === SDK_READY) { readyCbCount++; } + } else if (event === SDK_READY_FROM_CACHE && readinessManager.isReadyFromCache()) { + log.error(ERROR_CLIENT_LISTENER, ['SDK_READY_FROM_CACHE']); } });