Skip to content

Commit

Permalink
Removed ready promise wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilianoSanchez committed Oct 7, 2024
1 parent 7b6e433 commit 582dfe0
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 334 deletions.
3 changes: 2 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
- Added `factory.destroy()` method, which invokes the `destroy` method on all SDK clients created by the factory.
- Bugfixing - Fixed an issue with the server-side polling manager that caused dangling timers when the SDK was destroyed before it was ready.
- BREAKING CHANGES:
- Removed the deprecated `GOOGLE_ANALYTICS_TO_SPLIT` and `SPLIT_TO_GOOGLE_ANALYTICS` integrations.
- Updated default flag spec version to 1.2.
- Removed `/mySegments` endpoint from SplitAPI module, as it is replaced by `/memberships` endpoint.
- Removed support for MY_SEGMENTS_UPDATE and MY_SEGMENTS_UPDATE_V2 notification types, as they are replaced by MEMBERSHIPS_MS_UPDATE and MEMBERSHIPS_LS_UPDATE notification types.
- Removed the deprecated `GOOGLE_ANALYTICS_TO_SPLIT` and `SPLIT_TO_GOOGLE_ANALYTICS` integrations.
- Bugfixing - Fixed an issue with the `ready` method that caused the returned promise to hang on async/await syntax if it was rejected. The fix implies that the promise rejection must be handled by the user.

1.17.0 (September 6, 2024)
- Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks.
Expand Down
58 changes: 12 additions & 46 deletions src/readiness/__tests__/sdkReadinessManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function emitReadyEvent(readinessManager: IReadinessManager) {
readinessManager.segments.once.mock.calls[0][1]();
readinessManager.segments.on.mock.calls[0][1]();
readinessManager.gate.once.mock.calls[0][1]();
if (readinessManager.gate.once.mock.calls[3]) readinessManager.gate.once.mock.calls[3][1](); // ready promise
}

const timeoutErrorMessage = 'Split SDK emitted SDK_READY_TIMED_OUT event.';
Expand All @@ -32,6 +33,7 @@ const timeoutErrorMessage = 'Split SDK emitted SDK_READY_TIMED_OUT event.';
function emitTimeoutEvent(readinessManager: IReadinessManager) {
readinessManager.gate.once.mock.calls[1][1](timeoutErrorMessage);
readinessManager.hasTimedout = () => true;
if (readinessManager.gate.once.mock.calls[4]) readinessManager.gate.once.mock.calls[4][1](timeoutErrorMessage); // ready promise
}

describe('SDK Readiness Manager - Event emitter', () => {
Expand Down Expand Up @@ -245,8 +247,8 @@ describe('SDK Readiness Manager - Ready promise', () => {
}
);

// any subsequent call to .ready() must be a rejected promise
await readyForTimeout.then(
// any subsequent call to .ready() must be a rejected promise until the SDK is ready
await sdkReadinessManagerForTimedout.sdkStatus.ready().then(
() => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); },
() => {
expect('A subsequent call should be a rejected promise.');
Expand All @@ -258,7 +260,7 @@ describe('SDK Readiness Manager - Ready promise', () => {
emitReadyEvent(sdkReadinessManagerForTimedout.readinessManager);

// once SDK_READY, `.ready()` returns a resolved promise
await ready.then(
await sdkReadinessManagerForTimedout.sdkStatus.ready().then(
() => {
expect('It should be a resolved promise when the SDK is ready, even after an SDK timeout.');
loggerMock.mockClear();
Expand All @@ -270,57 +272,21 @@ describe('SDK Readiness Manager - Ready promise', () => {
});

test('Full blown 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];
let sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);

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.
emitReadyEvent(sdkReadinessManager.readinessManager);
expect(loggerMock.warn).toBeCalledWith(CLIENT_NO_LISTENER); // We should get a warning if the SDK get's ready before calling the ready method or attaching a listener to the ready event
loggerMock.warn.mockClear();

readyPromise.then(() => {
sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
sdkReadinessManager.sdkStatus.ready().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);
emitReadyEvent(sdkReadinessManager.readinessManager);
expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener or call the ready method, we get no warnings.
});
});
74 changes: 23 additions & 51 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 { IEventEmitter, ISettings } from '../types';
Expand Down Expand Up @@ -41,33 +40,21 @@ export function sdkReadinessManagerFactory(
});

/** Ready promise */
const readyPromise = generateReadyPromise();
let readyPromise: Promise<void>;

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

// 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);
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);
if (readyCbCount === internalReadyCbCount && !readyPromise) log.warn(CLIENT_NO_LISTENER);
});

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 @@ -91,34 +78,19 @@ export function sdkReadinessManagerFactory(
SDK_UPDATE,
SDK_READY_TIMED_OUT,
},
/**
* Returns a promise that will be resolved once the SDK has finished loading (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, calling the `ready` method after the
* SDK had timed out will return a new promise that should eventually resolve if the SDK gets 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
* }
* ```
*
* @function ready
* @returns {Promise<void>}
*/
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;
if (readinessManager.isReady()) return Promise.resolve();

if (readinessManager.hasTimedout()) return Promise.reject(new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.'));

return readyPromise || (readyPromise = new Promise<void>((resolve, reject) => {
readinessManager.gate.once(SDK_READY, () => {
resolve();
});
readinessManager.gate.once(SDK_READY_TIMED_OUT, (message: string) => {
reject(new Error(message));
});
}));
},

__getStatus() {
Expand Down
17 changes: 3 additions & 14 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,20 +390,9 @@ export interface IStatusInterface extends IEventEmitter {
*/
Event: EventConsts,
/**
* Returns a promise that will be resolved once the SDK has finished loading (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, calling the `ready` method after the
* SDK had timed out will return a new promise that should eventually resolve if the SDK gets 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 has finished loading (`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.
* You must handle the promise rejection to avoid an unhandled promise rejection error, or you can set the `startup.readyTimeout` configuration option to 0 to avoid the timeout and thus the rejection.
*
* @function ready
* @returns {Promise<void>}
Expand Down
162 changes: 0 additions & 162 deletions src/utils/promise/__tests__/wrapper.spec.ts

This file was deleted.

Loading

0 comments on commit 582dfe0

Please sign in to comment.