Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1eeff81
Add method to retrieve client readiness status synchronously
EmilianoSanchez Oct 1, 2025
bae6f71
Merge branch 'development' into readiness-status
EmilianoSanchez Oct 6, 2025
83f6e93
Merge pull request #434 from splitio/development
EmilianoSanchez Oct 7, 2025
6d68822
Use log level when logger is set
EmilianoSanchez Oct 8, 2025
3b99e8a
Merge pull request #436 from splitio/custom-logger
EmilianoSanchez Oct 8, 2025
9163c48
Fix typos
EmilianoSanchez Oct 14, 2025
1724ce8
Updated SDK_READY_FROM_CACHE event to be emitted alongside the SDK_RE…
EmilianoSanchez Oct 14, 2025
154c23f
Merge branch 'readiness-sdk-ready-from-cache' into readiness-status
EmilianoSanchez Oct 14, 2025
cfdd6e6
Merge remote-tracking branch 'origin/readiness-status' into readiness…
EmilianoSanchez Oct 14, 2025
53cc6db
feat: add whenReady and whenReadyFromCache methods to replace depreca…
EmilianoSanchez Oct 21, 2025
8b6a8d5
refactor: simplify SDK readiness checks
EmilianoSanchez Oct 22, 2025
96071a4
Merge pull request #438 from splitio/readiness-sdk-ready-from-cache
EmilianoSanchez Oct 22, 2025
2eb71a7
Merge branch 'readiness-baseline' into readiness-status
EmilianoSanchez Oct 22, 2025
1299349
Merge branch 'readiness-status' into readiness-fix-ready-promise
EmilianoSanchez Oct 22, 2025
13eaec3
feat: update whenReadyFromCache to return boolean indicating SDK read…
EmilianoSanchez Oct 22, 2025
e2179d7
Polishing
EmilianoSanchez Oct 22, 2025
e49de68
Revert "Add method to retrieve client readiness status synchronously"
EmilianoSanchez Oct 22, 2025
818235b
Revert "Revert "Add method to retrieve client readiness status synchr…
EmilianoSanchez Oct 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/logger/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 0 additions & 2 deletions src/logger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`');
Expand Down
2 changes: 1 addition & 1 deletion src/logger/messages/warn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'],
Expand Down
4 changes: 2 additions & 2 deletions src/readiness/__tests__/sdkReadinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});

Expand Down
7 changes: 3 additions & 4 deletions src/readiness/readinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(), {
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
32 changes: 30 additions & 2 deletions src/readiness/sdkReadinessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -93,18 +94,45 @@ 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();
}
}
return readyPromise;
},

__getStatus() {
whenReady() {
return new Promise<void>((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<boolean>((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(),
Expand Down
3 changes: 1 addition & 2 deletions src/readiness/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IStatusInterface } from '../types';
import SplitIO from '../../types/splitio';

/** Splits data emitter */
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/sdkClient/__tests__/clientInputValidation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const EVALUATION_RESULT = 'on';
const client: any = createClientMock(EVALUATION_RESULT);

const readinessManager: any = {
isReady: () => true,
isReadyFromCache: () => true,
isDestroyed: () => false
};

Expand Down
6 changes: 3 additions & 3 deletions src/sdkClient/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) :
Expand Down Expand Up @@ -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)) :
Expand Down Expand Up @@ -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({}) :
Expand Down
4 changes: 2 additions & 2 deletions src/sdkClient/clientInputValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
validateSplits,
validateTrafficType,
validateIfNotDestroyed,
validateIfOperational,
validateIfReadyFromCache,
validateEvaluationOptions
} from '../utils/inputValidation';
import { startsWith } from '../utils/lang';
Expand Down Expand Up @@ -46,7 +46,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
const isNotDestroyed = validateIfNotDestroyed(log, readinessManager, methodName);
const options = validateEvaluationOptions(log, maybeOptions, methodName);

validateIfOperational(log, readinessManager, methodName, nameOrNames);
validateIfReadyFromCache(log, readinessManager, methodName, nameOrNames);

const valid = isNotDestroyed && key && nameOrNames && attributes !== false;

Expand Down
5 changes: 3 additions & 2 deletions src/sdkManager/__tests__/index.asyncCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SplitIO from '../../../types/splitio';
const sdkReadinessManagerMock = {
readinessManager: {
isReady: jest.fn(() => true),
isReadyFromCache: jest.fn(() => true),
isDestroyed: jest.fn(() => false)
},
sdkStatus: jest.fn()
Expand Down Expand Up @@ -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([]);

Expand All @@ -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([]);
}
Expand Down
3 changes: 2 additions & 1 deletion src/sdkManager/__tests__/index.syncCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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([]);
}
Expand Down
8 changes: 4 additions & 4 deletions src/sdkManager/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -66,7 +66,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
*/
split(featureFlagName: string) {
const splitName = validateSplit(log, featureFlagName, SPLIT_FN_LABEL);
if (!validateIfNotDestroyed(log, readinessManager, SPLIT_FN_LABEL) || !validateIfOperational(log, readinessManager, SPLIT_FN_LABEL) || !splitName) {
if (!validateIfOperational(log, readinessManager, SPLIT_FN_LABEL) || !splitName) {
return isAsync ? Promise.resolve(null) : null;
}

Expand All @@ -87,7 +87,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
* Get the feature flag objects present on the factory storage
*/
splits() {
if (!validateIfNotDestroyed(log, readinessManager, SPLITS_FN_LABEL) || !validateIfOperational(log, readinessManager, SPLITS_FN_LABEL)) {
if (!validateIfOperational(log, readinessManager, SPLITS_FN_LABEL)) {
return isAsync ? Promise.resolve([]) : [];
}
const currentSplits = splits.getAll();
Expand All @@ -100,7 +100,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
* Get the feature flag names present on the factory storage
*/
names() {
if (!validateIfNotDestroyed(log, readinessManager, NAMES_FN_LABEL) || !validateIfOperational(log, readinessManager, NAMES_FN_LABEL)) {
if (!validateIfOperational(log, readinessManager, NAMES_FN_LABEL)) {
return isAsync ? Promise.resolve([]) : [];
}
const splitNames = splits.getSplitNames();
Expand Down
2 changes: 1 addition & 1 deletion src/sync/streaming/SSEHandler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/sync/submitters/telemetrySubmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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()) {
Expand Down
12 changes: 6 additions & 6 deletions src/trackers/telemetryTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
15 changes: 0 additions & 15 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Loading