diff --git a/CHANGES.txt b/CHANGES.txt index 711d3ca3..fdab3d6d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,11 @@ +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. + 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/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/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/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/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/readinessManager.ts b/src/readiness/readinessManager.ts index c69eedce..319e843d 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(), { @@ -91,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); @@ -115,9 +114,9 @@ export function readinessManagerFactory( isReady = true; try { syncLastUpdate(); - if (!isReadyFromCache && settings.storage?.type === STORAGE_LOCALSTORAGE) { + 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/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index ee558d47..03afd873 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,7 +106,33 @@ export function sdkReadinessManagerFactory( return readyPromise; }, - __getStatus() { + 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(readinessManager.isReady()); + } else if (readinessManager.hasTimedout()) { + reject(TIMEOUT_ERROR); + } else { + readinessManager.gate.once(SDK_READY_FROM_CACHE, () => resolve(readinessManager.isReady())); + readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR)); + } + }); + }, + + 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/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 { - 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) { 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/src/utils/inputValidation/__tests__/isOperational.spec.ts b/src/utils/inputValidation/__tests__/isOperational.spec.ts index bbc6afa7..19c1373a 100644 --- a/src/utils/inputValidation/__tests__/isOperational.spec.ts +++ b/src/utils/inputValidation/__tests__/isOperational.spec.ts @@ -1,7 +1,7 @@ -import { CLIENT_NOT_READY, ERROR_CLIENT_DESTROYED } from '../../../logger/constants'; +import { CLIENT_NOT_READY_FROM_CACHE, ERROR_CLIENT_DESTROYED } from '../../../logger/constants'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { validateIfNotDestroyed, validateIfOperational } from '../isOperational'; +import { validateIfNotDestroyed, validateIfReadyFromCache } from '../isOperational'; describe('validateIfNotDestroyed', () => { @@ -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; 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..b3884694 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; @@ -528,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'; }; @@ -694,6 +691,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, 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. */ @@ -703,7 +746,13 @@ declare namespace SplitIO { */ Event: EventConsts; /** - * 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). + * 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. * * 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. @@ -718,8 +767,24 @@ 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 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_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 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_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; } /** * Common definitions between clients for different environments interface. @@ -1653,7 +1718,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(); * ``` *