Skip to content

Commit d458b54

Browse files
Merge pull request #445 from splitio/readiness-fix-ready-promise
[Readiness] Deprecate `ready` promise and replace with `whenReady` and `whenReadyFromCache`
2 parents 96071a4 + 94814df commit d458b54

File tree

6 files changed

+154
-69
lines changed

6 files changed

+154
-69
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: 96 additions & 60 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,7 +64,8 @@ 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.
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.
5469
expect(typeof sdkStatus.__getStatus).toBe('function'); // The sdkStatus exposes a .__getStatus() function.
5570
expect(sdkStatus.__getStatus()).toEqual({
5671
isReady: false, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: false, lastUpdate: 0
@@ -67,9 +82,9 @@ describe('SDK Readiness Manager - Event emitter', () => {
6782
const sdkReadyResolvePromiseCall = gateMock.once.mock.calls[0];
6883
const sdkReadyRejectPromiseCall = gateMock.once.mock.calls[1];
6984
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.
85+
expect(sdkReadyResolvePromiseCall[0]).toBe(SDK_READY); // A one time only subscription is on the SDK_READY event
86+
expect(sdkReadyRejectPromiseCall[0]).toBe(SDK_READY_TIMED_OUT); // A one time only subscription is also on the SDK_READY_TIMED_OUT event
87+
expect(sdkReadyFromCacheListenersCheckCall[0]).toBe(SDK_READY_FROM_CACHE); // A one time only subscription is on the SDK_READY_FROM_CACHE event
7388

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

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

99114
emitReadyEvent(sdkReadinessManager.readinessManager);
100115

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

104119
expect(loggerMock.info).toBeCalledTimes(1); // If the SDK_READY event fires, we get a info message.
@@ -199,77 +214,98 @@ describe('SDK Readiness Manager - Event emitter', () => {
199214
});
200215
});
201216

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

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

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

210-
// make the SDK "ready"
231+
const readyFromCache = sdkReadinessManager.sdkStatus.whenReadyFromCache();
232+
const ready = sdkReadinessManager.sdkStatus.whenReady();
233+
234+
// make the SDK ready
211235
emitReadyEvent(sdkReadinessManager.readinessManager);
236+
expect(await sdkReadinessManager.sdkStatus.whenReadyFromCache()).toBe(true);
212237

213238
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-
);
239+
function incTestPassedCount() { testPassedCount++; }
240+
function throwTestFailed() { throw new Error('It should be resolved, not rejected.'); }
221241

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-
);
242+
await readyFromCache.then(incTestPassedCount, throwTestFailed);
243+
await ready.then(incTestPassedCount, throwTestFailed);
230244

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

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

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

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

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-
);
258+
emitTimeoutEvent(sdkReadinessManagerForTimedout.readinessManager); // make the SDK timeout
247259

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-
);
260+
let testPassedCount = 0;
261+
function incTestPassedCount() { testPassedCount++; }
262+
function throwTestFailed() { throw new Error('It should rejected, not resolved.'); }
263+
264+
await readyFromCacheForTimeout.then(throwTestFailed,incTestPassedCount);
265+
await readyForTimeout.then(throwTestFailed,incTestPassedCount);
256266

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

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

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

src/readiness/readinessManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function readinessManagerFactory(
9090
if (!isReady && !isDestroyed) {
9191
try {
9292
syncLastUpdate();
93-
gate.emit(SDK_READY_FROM_CACHE);
93+
gate.emit(SDK_READY_FROM_CACHE, isReady);
9494
} catch (e) {
9595
// throws user callback exceptions in next tick
9696
setTimeout(() => { throw e; }, 0);
@@ -116,7 +116,7 @@ export function readinessManagerFactory(
116116
syncLastUpdate();
117117
if (!isReadyFromCache) {
118118
isReadyFromCache = true;
119-
gate.emit(SDK_READY_FROM_CACHE);
119+
gate.emit(SDK_READY_FROM_CACHE, isReady);
120120
}
121121
gate.emit(SDK_READY);
122122
} catch (e) {

src/readiness/sdkReadinessManager.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +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(SDK_READY_TIMED_OUT);
1213

1314
/**
1415
* SdkReadinessManager factory, which provides the public status API of SDK clients and manager: ready promise, readiness event emitter and constants (SDK_READY, etc).
@@ -38,6 +39,8 @@ export function sdkReadinessManagerFactory(
3839
} else if (event === SDK_READY) {
3940
readyCbCount++;
4041
}
42+
} else if (event === SDK_READY_FROM_CACHE && readinessManager.isReadyFromCache()) {
43+
log.error(ERROR_CLIENT_LISTENER, ['SDK_READY_FROM_CACHE']);
4144
}
4245
});
4346

@@ -93,6 +96,7 @@ export function sdkReadinessManagerFactory(
9396
SDK_READY_TIMED_OUT,
9497
},
9598

99+
// @TODO: remove in next major
96100
ready() {
97101
if (readinessManager.hasTimedout()) {
98102
if (!readinessManager.isReady()) {
@@ -104,6 +108,32 @@ export function sdkReadinessManagerFactory(
104108
return readyPromise;
105109
},
106110

111+
whenReady() {
112+
return new Promise<void>((resolve, reject) => {
113+
if (readinessManager.isReady()) {
114+
resolve();
115+
} else if (readinessManager.hasTimedout()) {
116+
reject(TIMEOUT_ERROR);
117+
} else {
118+
readinessManager.gate.once(SDK_READY, resolve);
119+
readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR));
120+
}
121+
});
122+
},
123+
124+
whenReadyFromCache() {
125+
return new Promise<boolean>((resolve, reject) => {
126+
if (readinessManager.isReadyFromCache()) {
127+
resolve(readinessManager.isReady());
128+
} else if (readinessManager.hasTimedout()) {
129+
reject(TIMEOUT_ERROR);
130+
} else {
131+
readinessManager.gate.once(SDK_READY_FROM_CACHE, () => resolve(readinessManager.isReady()));
132+
readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR));
133+
}
134+
});
135+
},
136+
107137
__getStatus() {
108138
return {
109139
isReady: readinessManager.isReady(),

0 commit comments

Comments
 (0)