diff --git a/CHANGES.txt b/CHANGES.txt index 9163d52f..a5e76102 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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. diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index 4d47d12f..341f37da 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -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); - }); -}); diff --git a/src/readiness/sdkReadinessManager.ts b/src/readiness/sdkReadinessManager.ts index 64e518b3..bfc2217c 100644 --- a/src/readiness/sdkReadinessManager.ts +++ b/src/readiness/sdkReadinessManager.ts @@ -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'; @@ -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((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, @@ -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((resolve, reject) => { if (readinessManager.isReady()) { diff --git a/src/utils/promise/__tests__/wrapper.spec.ts b/src/utils/promise/__tests__/wrapper.spec.ts deleted file mode 100644 index ab44f9d2..00000000 --- a/src/utils/promise/__tests__/wrapper.spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -// @ts-nocheck -import { promiseWrapper } from '../wrapper'; - -test('Promise utils / promise wrapper', function (done) { - expect.assertions(58); // number of passHandler, passHandlerFinally, passHandlerWithThrow and `hasOnFulfilled` asserts - - const value = 'value'; - const failHandler = (val) => { done.fail(val); }; - const passHandler = (val) => { expect(val).toBe(value); return val; }; - const passHandlerFinally = (val) => { expect(val).toBeUndefined(); }; - const passHandlerWithThrow = (val) => { expect(val).toBe(value); throw val; }; - const createResolvedPromise = () => new Promise((res) => { setTimeout(() => { res(value); }, 100); }); - const createRejectedPromise = () => new Promise((_, rej) => { setTimeout(() => { rej(value); }, 100); }); - - // resolved promises - let wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(false); - - wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); - wrappedPromise.then(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); - wrappedPromise.finally(passHandlerFinally); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); - wrappedPromise.then(passHandler, failHandler).finally(passHandlerFinally); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); - wrappedPromise.then(passHandler).catch(failHandler).finally(passHandlerFinally); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); - wrappedPromise.then(passHandler).catch(failHandler).then(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); - wrappedPromise.then(passHandler).then(passHandler).catch(failHandler).finally(passHandlerFinally).then(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); - wrappedPromise.then(passHandler).then(passHandlerWithThrow).catch(passHandler).then(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - const wrappedPromise2 = promiseWrapper(createResolvedPromise(), failHandler); - wrappedPromise2.then(() => { - wrappedPromise2.then(passHandler); - }); - expect(wrappedPromise2.hasOnFulfilled()).toBe(true); - - Promise.all([ - promiseWrapper(createResolvedPromise(), failHandler), - promiseWrapper(createResolvedPromise(), failHandler)] - ).then((val) => { expect(val).toEqual([value, value]); }); - - // rejected promises - wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(false); - - wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise.catch(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(false); - - wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise.catch(passHandler).then(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(false); - - // caveat: setting an `onFinally` handler as the first handler, requires an `onRejected` handler if promise is rejected - wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise.finally(passHandlerFinally).catch(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); - wrappedPromise.then(undefined, passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(false); - - wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); - wrappedPromise.then(failHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise.then(failHandler).then(failHandler).catch(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); - wrappedPromise.then(failHandler).then(failHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise.then(failHandler, passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise.then(failHandler).catch(passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise.then(failHandler).then(failHandler, passHandler); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise.then(failHandler).catch(passHandler).then(passHandler).finally(passHandlerFinally); - expect(wrappedPromise.hasOnFulfilled()).toBe(true); - - const wrappedPromise3 = promiseWrapper(createRejectedPromise(), failHandler); - wrappedPromise3.catch(() => { - wrappedPromise3.catch(passHandler); - }); - expect(wrappedPromise3.hasOnFulfilled()).toBe(false); - - Promise.all([ - promiseWrapper(createResolvedPromise(), failHandler), - promiseWrapper(createRejectedPromise(), failHandler)]).catch(passHandler); - - setTimeout(() => { - done(); - }, 1000); - -}); - -test('Promise utils / promise wrapper: async/await', async () => { - - expect.assertions(8); // number of passHandler, passHandlerWithThrow and passHandlerFinally - - const value = 'value'; - const failHandler = (val) => { throw val; }; - const passHandler = (val) => { expect(val).toBe(value); return val; }; - const passHandlerFinally = (val) => { expect(val).toBeUndefined(); }; - const passHandlerWithThrow = (val) => { expect(val).toBe(value); throw val; }; - const createResolvedPromise = () => new Promise((res) => { res(value); }); - const createRejectedPromise = () => new Promise((res, rej) => { rej(value); }); - - try { - const result = await promiseWrapper(createResolvedPromise(), failHandler); - passHandler(result); - } catch (result) { - failHandler(result); - } finally { - passHandlerFinally(); - } - - try { - const result = await promiseWrapper(createRejectedPromise(), failHandler); - failHandler(result); - } catch (result) { - passHandler(result); - } - - let result; - try { - result = await promiseWrapper(createResolvedPromise(), failHandler); - passHandler(result); - passHandlerWithThrow(result); - } catch (error) { - result = passHandler(error); - } finally { - passHandlerFinally(); - } - passHandler(result); -}); diff --git a/src/utils/promise/wrapper.ts b/src/utils/promise/wrapper.ts deleted file mode 100644 index 62266457..00000000 --- a/src/utils/promise/wrapper.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * wraps a given promise in a new one with a default onRejected function, - * that handles the promise rejection if not other onRejected handler is provided. - * - * Caveats: - * - There are some cases where the `defaultOnRejected` handler is not invoked - * and the promise rejection must be handled by the user (same as the Promise spec): - * - using async/await syntax with a transpiler to Promises - * - setting an `onFinally` handler as the first handler (e.g. `promiseWrapper(Promise.reject()).finally(...)`) - * - setting more than one handler with at least one of them being an onRejected handler - * - If the wrapped promise is rejected when using native async/await syntax, the `defaultOnRejected` handler is invoked - * and neither the catch block nor the remaining try block are executed. - * - * @param customPromise - promise to wrap - * @param defaultOnRejected - default onRejected function - * @returns a promise that doesn't need to be handled for rejection (except when using async/await syntax) and - * includes a method named `hasOnFulfilled` that returns true if the promise has attached an onFulfilled handler. - */ -export function promiseWrapper(customPromise: Promise, defaultOnRejected: (_: any) => any): Promise & { hasOnFulfilled: () => boolean } { - - let hasOnFulfilled = false; - let hasOnRejected = false; - - function chain(promise: Promise): Promise { - const newPromise: Promise = new Promise((res, rej) => { - return promise.then( - res, - function (value) { - if (hasOnRejected) { - rej(value); - } else { - defaultOnRejected(value); - } - } - ); - }); - - const originalThen = newPromise.then; - - // Using `defineProperty` in case Promise.prototype.then property is not writable - Object.defineProperty(newPromise, 'then', { - value: function (onfulfilled: any, onrejected: any) { - const result: Promise = originalThen.call(newPromise, onfulfilled, onrejected); - if (typeof onfulfilled === 'function') hasOnFulfilled = true; - if (typeof onrejected === 'function') { - hasOnRejected = true; - return result; - } else { - return chain(result); - } - } - }); - - return newPromise; - } - - const result = chain(customPromise) as Promise & { hasOnFulfilled: () => boolean }; - result.hasOnFulfilled = () => hasOnFulfilled; - return result; -} diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 49f70c62..e61ffb22 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -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; /** * 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.