Skip to content

Commit 4230511

Browse files
Merge branch 'readiness-fix-ready-promise' into readiness-status
2 parents 818235b + 94814df commit 4230511

File tree

5 files changed

+108
-68
lines changed

5 files changed

+108
-68
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
2.8.0 (October XX, 2025)
2+
- 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.
23
- Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted.
34

45
2.7.1 (October 8, 2025)

src/logger/messages/warn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const codesWarn: [number, string][] = codesError.concat([
1515
[c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s, keeping data to retry on next iteration. Reason: %s.'],
1616
// client status
1717
[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.'],
18-
[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.'],
18+
[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.'],
1919
// input validation
2020
[c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'],
2121
[c.WARN_TRIMMING_PROPERTIES, '%s: more than 300 properties were provided. Some of them will be trimmed when processed.'],

src/readiness/__tests__/sdkReadinessManager.spec.ts

Lines changed: 97 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// @ts-nocheck
22
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
33
import SplitIO from '../../../types/splitio';
4-
import { SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT, SDK_UPDATE } from '../constants';
4+
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';
55
import { sdkReadinessManagerFactory } from '../sdkReadinessManager';
66
import { IReadinessManager } from '../types';
77
import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO_LISTENER } from '../../logger/constants';
88
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';
9+
import { EventEmitter } from '../../utils/MinEvents';
910

1011
const EventEmitterMock = jest.fn(() => ({
1112
on: jest.fn(),
@@ -19,24 +20,37 @@ const EventEmitterMock = jest.fn(() => ({
1920

2021
// Makes readinessManager emit SDK_READY & update isReady flag
2122
function emitReadyEvent(readinessManager: IReadinessManager) {
23+
if (readinessManager.gate instanceof EventEmitter) {
24+
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
25+
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED);
26+
return;
27+
}
28+
2229
readinessManager.splits.once.mock.calls[0][1]();
2330
readinessManager.splits.on.mock.calls[0][1]();
2431
readinessManager.segments.once.mock.calls[0][1]();
2532
readinessManager.segments.on.mock.calls[0][1]();
2633
readinessManager.gate.once.mock.calls[0][1]();
34+
if (readinessManager.gate.once.mock.calls[3]) readinessManager.gate.once.mock.calls[3][1](); // whenReady promise
2735
}
2836

2937
const timeoutErrorMessage = 'Split SDK emitted SDK_READY_TIMED_OUT event.';
3038

3139
// Makes readinessManager emit SDK_READY_TIMED_OUT & update hasTimedout flag
3240
function emitTimeoutEvent(readinessManager: IReadinessManager) {
41+
if (readinessManager.gate instanceof EventEmitter) {
42+
readinessManager.timeout();
43+
return;
44+
}
45+
3346
readinessManager.gate.once.mock.calls[1][1](timeoutErrorMessage);
3447
readinessManager.hasTimedout = () => true;
48+
if (readinessManager.gate.once.mock.calls[4]) readinessManager.gate.once.mock.calls[4][1](timeoutErrorMessage); // whenReady promise
3549
}
3650

3751
describe('SDK Readiness Manager - Event emitter', () => {
3852

39-
afterEach(() => { loggerMock.mockClear(); });
53+
beforeEach(() => { loggerMock.mockClear(); });
4054

4155
test('Providing the gate object to get the SDK status interface that manages events', () => {
4256
expect(typeof sdkReadinessManagerFactory).toBe('function'); // The module exposes a function.
@@ -50,9 +64,9 @@ describe('SDK Readiness Manager - Event emitter', () => {
5064
expect(sdkStatus[propName]).toBeTruthy(); // The sdkStatus exposes all minimal EventEmitter functionality.
5165
});
5266

53-
expect(typeof sdkStatus.ready).toBe('function'); // The sdkStatus exposes a .ready() function.
54-
expect(typeof sdkStatus.getStatus).toBe('function'); // The sdkStatus exposes a .getStatus() function.
55-
expect(sdkStatus.getStatus()).toEqual({
67+
expect(typeof sdkStatus.whenReady).toBe('function'); // The sdkStatus exposes a .whenReady() function.
68+
expect(typeof sdkStatus.whenReadyFromCache).toBe('function'); // The sdkStatus exposes a .whenReadyFromCache() function.
69+
expect(sdkStatus.getStatus()).toEqual({ // The sdkStatus exposes a .getStatus() function.
5670
isReady: false, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: false, lastUpdate: 0
5771
});
5872

@@ -67,9 +81,9 @@ describe('SDK Readiness Manager - Event emitter', () => {
6781
const sdkReadyResolvePromiseCall = gateMock.once.mock.calls[0];
6882
const sdkReadyRejectPromiseCall = gateMock.once.mock.calls[1];
6983
const sdkReadyFromCacheListenersCheckCall = gateMock.once.mock.calls[2];
70-
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.
71-
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.
72-
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.
84+
expect(sdkReadyResolvePromiseCall[0]).toBe(SDK_READY); // A one time only subscription is on the SDK_READY event
85+
expect(sdkReadyRejectPromiseCall[0]).toBe(SDK_READY_TIMED_OUT); // A one time only subscription is also on the SDK_READY_TIMED_OUT event
86+
expect(sdkReadyFromCacheListenersCheckCall[0]).toBe(SDK_READY_FROM_CACHE); // A one time only subscription is on the SDK_READY_FROM_CACHE event
7387

7488
expect(gateMock.on).toBeCalledTimes(2); // It should also add two persistent listeners
7589

@@ -98,7 +112,7 @@ describe('SDK Readiness Manager - Event emitter', () => {
98112

99113
emitReadyEvent(sdkReadinessManager.readinessManager);
100114

101-
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.
115+
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.
102116
expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // Telling us there were no listeners and evaluations before this point may have been incorrect.
103117

104118
expect(loggerMock.info).toBeCalledTimes(1); // If the SDK_READY event fires, we get a info message.
@@ -199,77 +213,98 @@ describe('SDK Readiness Manager - Event emitter', () => {
199213
});
200214
});
201215

202-
describe('SDK Readiness Manager - Ready promise', () => {
216+
describe('SDK Readiness Manager - Promises', () => {
203217

204-
test('.ready() promise behavior for clients', async () => {
205-
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
218+
test('.whenReady() and .whenReadyFromCache() promises resolves when SDK_READY is emitted', async () => {
219+
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings);
220+
221+
// make the SDK ready from cache
222+
sdkReadinessManager.readinessManager.splits.emit(SDK_SPLITS_CACHE_LOADED);
223+
expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(false);
206224

207-
const ready = sdkReadinessManager.sdkStatus.ready();
208-
expect(ready instanceof Promise).toBe(true); // It should return a promise.
225+
// validate error log for SDK_READY_FROM_CACHE
226+
expect(loggerMock.error).not.toBeCalled();
227+
sdkReadinessManager.readinessManager.gate.on(SDK_READY_FROM_CACHE, () => {});
228+
expect(loggerMock.error).toBeCalledWith(ERROR_CLIENT_LISTENER, ['SDK_READY_FROM_CACHE']);
209229

210-
// make the SDK "ready"
230+
const readyFromCache = sdkReadinessManager.sdkStatus.whenReadyFromCache();
231+
const ready = sdkReadinessManager.sdkStatus.whenReady();
232+
233+
// make the SDK ready
211234
emitReadyEvent(sdkReadinessManager.readinessManager);
235+
expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(true);
212236

213237
let testPassedCount = 0;
214-
await ready.then(
215-
() => {
216-
expect('It should be a promise that will be resolved when the SDK is ready.');
217-
testPassedCount++;
218-
},
219-
() => { throw new Error('It should be resolved on ready event, not rejected.'); }
220-
);
238+
function incTestPassedCount() { testPassedCount++; }
239+
function throwTestFailed() { throw new Error('It should be resolved, not rejected.'); }
221240

222-
// any subsequent call to .ready() must be a resolved promise
223-
await ready.then(
224-
() => {
225-
expect('A subsequent call should be a resolved promise.');
226-
testPassedCount++;
227-
},
228-
() => { throw new Error('It should be resolved on ready event, not rejected.'); }
229-
);
241+
await readyFromCache.then(incTestPassedCount, throwTestFailed);
242+
await ready.then(incTestPassedCount, throwTestFailed);
230243

231-
// control assertion. stubs already reset.
232-
expect(testPassedCount).toBe(2);
244+
// any subsequent call to .whenReady() and .whenReadyFromCache() must be a resolved promise
245+
await sdkReadinessManager.sdkStatus.whenReady().then(incTestPassedCount, throwTestFailed);
246+
await sdkReadinessManager.sdkStatus.whenReadyFromCache().then(incTestPassedCount, throwTestFailed);
233247

234-
const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
248+
expect(testPassedCount).toBe(4);
249+
});
235250

236-
const readyForTimeout = sdkReadinessManagerForTimedout.sdkStatus.ready();
251+
test('.whenReady() and .whenReadyFromCache() promises reject when SDK_READY_TIMED_OUT is emitted before SDK_READY', async () => {
252+
const sdkReadinessManagerForTimedout = sdkReadinessManagerFactory(EventEmitter, fullSettings);
237253

238-
emitTimeoutEvent(sdkReadinessManagerForTimedout.readinessManager); // make the SDK "timed out"
254+
const readyFromCacheForTimeout = sdkReadinessManagerForTimedout.sdkStatus.whenReadyFromCache();
255+
const readyForTimeout = sdkReadinessManagerForTimedout.sdkStatus.whenReady();
239256

240-
await readyForTimeout.then(
241-
() => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); },
242-
() => {
243-
expect('It should be a promise that will be rejected when the SDK is timed out.');
244-
testPassedCount++;
245-
}
246-
);
257+
emitTimeoutEvent(sdkReadinessManagerForTimedout.readinessManager); // make the SDK timeout
247258

248-
// any subsequent call to .ready() must be a rejected promise
249-
await readyForTimeout.then(
250-
() => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); },
251-
() => {
252-
expect('A subsequent call should be a rejected promise.');
253-
testPassedCount++;
254-
}
255-
);
259+
let testPassedCount = 0;
260+
function incTestPassedCount() { testPassedCount++; }
261+
function throwTestFailed() { throw new Error('It should rejected, not resolved.'); }
262+
263+
await readyFromCacheForTimeout.then(throwTestFailed,incTestPassedCount);
264+
await readyForTimeout.then(throwTestFailed,incTestPassedCount);
256265

257-
// make the SDK "ready"
266+
// any subsequent call to .whenReady() and .whenReadyFromCache() must be a rejected promise until the SDK is ready
267+
await sdkReadinessManagerForTimedout.sdkStatus.whenReadyFromCache().then(throwTestFailed,incTestPassedCount);
268+
await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then(throwTestFailed,incTestPassedCount);
269+
270+
// make the SDK ready
258271
emitReadyEvent(sdkReadinessManagerForTimedout.readinessManager);
259272

260-
// once SDK_READY, `.ready()` returns a resolved promise
261-
await ready.then(
262-
() => {
263-
expect('It should be a resolved promise when the SDK is ready, even after an SDK timeout.');
264-
loggerMock.mockClear();
265-
testPassedCount++;
266-
expect(testPassedCount).toBe(5);
267-
},
268-
() => { throw new Error('It should be resolved on ready event, not rejected.'); }
269-
);
273+
// once SDK_READY, `.whenReady()` returns a resolved promise
274+
await sdkReadinessManagerForTimedout.sdkStatus.whenReady().then(incTestPassedCount, throwTestFailed);
275+
await sdkReadinessManagerForTimedout.sdkStatus.whenReadyFromCache().then(incTestPassedCount, throwTestFailed);
276+
277+
expect(testPassedCount).toBe(6);
278+
});
279+
280+
test('whenReady promise counts as an SDK_READY listener', (done) => {
281+
let sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings);
282+
283+
emitReadyEvent(sdkReadinessManager.readinessManager);
284+
285+
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
286+
loggerMock.warn.mockClear();
287+
288+
sdkReadinessManager = sdkReadinessManagerFactory(EventEmitter, fullSettings);
289+
sdkReadinessManager.sdkStatus.whenReady().then(() => {
290+
expect('whenReady promise is resolved when the gate emits SDK_READY.');
291+
done();
292+
}, () => {
293+
throw new Error('This should not be called as the promise is being resolved.');
294+
});
295+
296+
emitReadyEvent(sdkReadinessManager.readinessManager);
297+
298+
expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener or call the whenReady method, we get no warnings.
270299
});
300+
});
301+
302+
// @TODO: remove in next major
303+
describe('SDK Readiness Manager - Ready promise', () => {
304+
305+
beforeEach(() => { loggerMock.mockClear(); });
271306

272-
test('Full blown ready promise count as a callback and resolves on SDK_READY', (done) => {
307+
test('ready promise count as a callback and resolves on SDK_READY', (done) => {
273308
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
274309
const readyPromise = sdkReadinessManager.sdkStatus.ready();
275310

src/readiness/sdkReadinessManager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO
99

1010
const NEW_LISTENER_EVENT = 'newListener';
1111
const REMOVE_LISTENER_EVENT = 'removeListener';
12-
const TIMEOUT_ERROR = new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.');
12+
const TIMEOUT_ERROR = new Error(SDK_READY_TIMED_OUT);
1313

1414
/**
1515
* SdkReadinessManager factory, which provides the public status API of SDK clients and manager: ready promise, readiness event emitter and constants (SDK_READY, etc).
@@ -39,6 +39,8 @@ export function sdkReadinessManagerFactory(
3939
} else if (event === SDK_READY) {
4040
readyCbCount++;
4141
}
42+
} else if (event === SDK_READY_FROM_CACHE && readinessManager.isReadyFromCache()) {
43+
log.error(ERROR_CLIENT_LISTENER, ['SDK_READY_FROM_CACHE']);
4244
}
4345
});
4446

@@ -98,7 +100,7 @@ export function sdkReadinessManagerFactory(
98100
ready() {
99101
if (readinessManager.hasTimedout()) {
100102
if (!readinessManager.isReady()) {
101-
return promiseWrapper(Promise.reject(TIMEOUT_ERROR), defaultOnRejected);
103+
return promiseWrapper(Promise.reject(new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.')), defaultOnRejected);
102104
} else {
103105
return Promise.resolve();
104106
}

types/splitio.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ declare namespace SplitIO {
752752
*/
753753
getStatus(): ReadinessStatus;
754754
/**
755-
* 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).
755+
* 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).
756756
* 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.
757757
*
758758
* 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.
@@ -771,15 +771,17 @@ declare namespace SplitIO {
771771
*/
772772
ready(): Promise<void>;
773773
/**
774-
* 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).
774+
* 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).
775775
* 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.
776+
* 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.
776777
*
777778
* @returns A promise that resolves once the SDK_READY event is emitted or rejects if the SDK has timedout.
778779
*/
779780
whenReady(): Promise<void>;
780781
/**
781-
* 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).
782+
* 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).
782783
* 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.
784+
* 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.
783785
*
784786
* @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
785787
* 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.

0 commit comments

Comments
 (0)