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