Skip to content
4 changes: 4 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
3.0.0 (XXX XX, 2025)
- BREAKING CHANGES:
- Removed the deprecated `client.ready()` method. Use `client.whenReady()` or `client.whenReadyFromCache()` instead.

2.8.0 (October XX, 2025)
- 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.
- Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted.
Expand Down
61 changes: 0 additions & 61 deletions src/readiness/__tests__/sdkReadinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,64 +299,3 @@ describe('SDK Readiness Manager - Promises', () => {
expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener or call the whenReady method, we get no warnings.
});
});

// @TODO: remove in next major
describe('SDK Readiness Manager - Ready promise', () => {

beforeEach(() => { loggerMock.mockClear(); });

test('ready promise count as a callback and resolves on SDK_READY', (done) => {
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
const readyPromise = sdkReadinessManager.sdkStatus.ready();

// Get the callback
const readyEventCB = sdkReadinessManager.readinessManager.gate.once.mock.calls[0][1];

readyEventCB();
expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // We would get the warning if the SDK get\'s ready before attaching any callbacks to ready promise.
loggerMock.warn.mockClear();

readyPromise.then(() => {
expect('The ready promise is resolved when the gate emits SDK_READY.');
done();
}, () => {
throw new Error('This should not be called as the promise is being resolved.');
});

readyEventCB();
expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener there are no warnings.
});

test('.ready() rejected promises have a default onRejected handler that just logs the error', (done) => {
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
let readyForTimeout = sdkReadinessManager.sdkStatus.ready();

emitTimeoutEvent(sdkReadinessManager.readinessManager); // make the SDK "timed out"

readyForTimeout.then(
() => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); }
);

expect(loggerMock.error).not.toBeCalled(); // not called until promise is rejected

setTimeout(() => {
expect(loggerMock.error.mock.calls).toEqual([[timeoutErrorMessage]]); // If we don\'t handle the rejected promise, an error is logged.
readyForTimeout = sdkReadinessManager.sdkStatus.ready();

setTimeout(() => {
expect(loggerMock.error).lastCalledWith('Split SDK has emitted SDK_READY_TIMED_OUT event.'); // If we don\'t handle a new .ready() rejected promise, an error is logged.
readyForTimeout = sdkReadinessManager.sdkStatus.ready();

readyForTimeout
.then(() => { throw new Error(); })
.then(() => { throw new Error(); })
.catch((error) => {
expect(error instanceof Error).toBe(true);
expect(error.message).toBe('Split SDK has emitted SDK_READY_TIMED_OUT event.');
expect(loggerMock.error).toBeCalledTimes(2); // If we provide an onRejected handler, even chaining several onFulfilled handlers, the error is not logged.
done();
});
}, 0);
}, 0);
});
});
46 changes: 9 additions & 37 deletions src/readiness/sdkReadinessManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { objectAssign } from '../utils/lang/objectAssign';
import { promiseWrapper } from '../utils/promise/wrapper';
import { readinessManagerFactory } from './readinessManager';
import { ISdkReadinessManager } from './types';
import { ISettings } from '../types';
Expand Down Expand Up @@ -44,34 +43,19 @@ export function sdkReadinessManagerFactory(
}
});

/** Ready promise */
const readyPromise = generateReadyPromise();
readinessManager.gate.once(SDK_READY, () => {
log.info(CLIENT_READY);

readinessManager.gate.once(SDK_READY_FROM_CACHE, () => {
log.info(CLIENT_READY_FROM_CACHE);
if (readyCbCount === internalReadyCbCount) log.warn(CLIENT_NO_LISTENER);
});

// default onRejected handler, that just logs the error, if ready promise doesn't have one.
function defaultOnRejected(err: any) {
log.error(err && err.message);
}

function generateReadyPromise() {
const promise = promiseWrapper(new Promise<void>((resolve, reject) => {
readinessManager.gate.once(SDK_READY, () => {
log.info(CLIENT_READY);

if (readyCbCount === internalReadyCbCount && !promise.hasOnFulfilled()) log.warn(CLIENT_NO_LISTENER);
resolve();
});
readinessManager.gate.once(SDK_READY_TIMED_OUT, (message: string) => {
reject(new Error(message));
});
}), defaultOnRejected);

return promise;
}
readinessManager.gate.once(SDK_READY_TIMED_OUT, (message: string) => {
log.error(message);
});

readinessManager.gate.once(SDK_READY_FROM_CACHE, () => {
log.info(CLIENT_READY_FROM_CACHE);
});

return {
readinessManager,
Expand All @@ -96,18 +80,6 @@ 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);
} else {
return Promise.resolve();
}
}
return readyPromise;
},

whenReady() {
return new Promise<void>((resolve, reject) => {
if (readinessManager.isReady()) {
Expand Down
162 changes: 0 additions & 162 deletions src/utils/promise/__tests__/wrapper.spec.ts

This file was deleted.

60 changes: 0 additions & 60 deletions src/utils/promise/wrapper.ts

This file was deleted.

19 changes: 0 additions & 19 deletions types/splitio.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,25 +699,6 @@ declare namespace SplitIO {
* Constant object containing the SDK events for you to use.
*/
Event: EventConsts;
/**
* 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).
* 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.
* However, when using async/await syntax, the rejection should be explicitly propagated like in the following example:
* ```
* try {
* await client.ready().catch((e) => { throw e; });
* // SDK is ready
* } catch(e) {
* // SDK has timedout
* }
* ```
*
* @returns A promise that resolves once the SDK is ready or rejects if the SDK has timedout.
* @deprecated Use `whenReady` instead.
*/
ready(): Promise<void>;
/**
* 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).
* 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.
Expand Down