Skip to content

Commit 582dfe0

Browse files
Removed ready promise wrapper
1 parent 7b6e433 commit 582dfe0

File tree

6 files changed

+40
-334
lines changed

6 files changed

+40
-334
lines changed

CHANGES.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
- Added `factory.destroy()` method, which invokes the `destroy` method on all SDK clients created by the factory.
44
- Bugfixing - Fixed an issue with the server-side polling manager that caused dangling timers when the SDK was destroyed before it was ready.
55
- BREAKING CHANGES:
6-
- Removed the deprecated `GOOGLE_ANALYTICS_TO_SPLIT` and `SPLIT_TO_GOOGLE_ANALYTICS` integrations.
76
- Updated default flag spec version to 1.2.
87
- Removed `/mySegments` endpoint from SplitAPI module, as it is replaced by `/memberships` endpoint.
98
- 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.
9+
- Removed the deprecated `GOOGLE_ANALYTICS_TO_SPLIT` and `SPLIT_TO_GOOGLE_ANALYTICS` integrations.
10+
- 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.
1011

1112
1.17.0 (September 6, 2024)
1213
- Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks.

src/readiness/__tests__/sdkReadinessManager.spec.ts

Lines changed: 12 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function emitReadyEvent(readinessManager: IReadinessManager) {
2424
readinessManager.segments.once.mock.calls[0][1]();
2525
readinessManager.segments.on.mock.calls[0][1]();
2626
readinessManager.gate.once.mock.calls[0][1]();
27+
if (readinessManager.gate.once.mock.calls[3]) readinessManager.gate.once.mock.calls[3][1](); // ready promise
2728
}
2829

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

3739
describe('SDK Readiness Manager - Event emitter', () => {
@@ -245,8 +247,8 @@ describe('SDK Readiness Manager - Ready promise', () => {
245247
}
246248
);
247249

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

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

272274
test('Full blown ready promise count as a callback and resolves on SDK_READY', (done) => {
273-
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
274-
const readyPromise = sdkReadinessManager.sdkStatus.ready();
275-
276-
// Get the callback
277-
const readyEventCB = sdkReadinessManager.readinessManager.gate.once.mock.calls[0][1];
275+
let sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
278276

279-
readyEventCB();
280-
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.
277+
emitReadyEvent(sdkReadinessManager.readinessManager);
278+
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
281279
loggerMock.warn.mockClear();
282280

283-
readyPromise.then(() => {
281+
sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
282+
sdkReadinessManager.sdkStatus.ready().then(() => {
284283
expect('The ready promise is resolved when the gate emits SDK_READY.');
285284
done();
286285
}, () => {
287286
throw new Error('This should not be called as the promise is being resolved.');
288287
});
289288

290-
readyEventCB();
291-
expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener there are no warnings.
292-
});
293-
294-
test('.ready() rejected promises have a default onRejected handler that just logs the error', (done) => {
295-
const sdkReadinessManager = sdkReadinessManagerFactory(EventEmitterMock, fullSettings);
296-
let readyForTimeout = sdkReadinessManager.sdkStatus.ready();
297-
298-
emitTimeoutEvent(sdkReadinessManager.readinessManager); // make the SDK "timed out"
299-
300-
readyForTimeout.then(
301-
() => { throw new Error('It should be a promise that was rejected on SDK_READY_TIMED_OUT, not resolved.'); }
302-
);
303-
304-
expect(loggerMock.error).not.toBeCalled(); // not called until promise is rejected
305-
306-
setTimeout(() => {
307-
expect(loggerMock.error.mock.calls).toEqual([[timeoutErrorMessage]]); // If we don\'t handle the rejected promise, an error is logged.
308-
readyForTimeout = sdkReadinessManager.sdkStatus.ready();
309-
310-
setTimeout(() => {
311-
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.
312-
readyForTimeout = sdkReadinessManager.sdkStatus.ready();
313-
314-
readyForTimeout
315-
.then(() => { throw new Error(); })
316-
.then(() => { throw new Error(); })
317-
.catch((error) => {
318-
expect(error instanceof Error).toBe(true);
319-
expect(error.message).toBe('Split SDK has emitted SDK_READY_TIMED_OUT event.');
320-
expect(loggerMock.error).toBeCalledTimes(2); // If we provide an onRejected handler, even chaining several onFulfilled handlers, the error is not logged.
321-
done();
322-
});
323-
}, 0);
324-
}, 0);
289+
emitReadyEvent(sdkReadinessManager.readinessManager);
290+
expect(loggerMock.warn).not.toBeCalled(); // But if we have a listener or call the ready method, we get no warnings.
325291
});
326292
});

src/readiness/sdkReadinessManager.ts

Lines changed: 23 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { objectAssign } from '../utils/lang/objectAssign';
2-
import { promiseWrapper } from '../utils/promise/wrapper';
32
import { readinessManagerFactory } from './readinessManager';
43
import { ISdkReadinessManager } from './types';
54
import { IEventEmitter, ISettings } from '../types';
@@ -41,33 +40,21 @@ export function sdkReadinessManagerFactory(
4140
});
4241

4342
/** Ready promise */
44-
const readyPromise = generateReadyPromise();
43+
let readyPromise: Promise<void>;
4544

46-
readinessManager.gate.once(SDK_READY_FROM_CACHE, () => {
47-
log.info(CLIENT_READY_FROM_CACHE);
48-
});
49-
50-
// default onRejected handler, that just logs the error, if ready promise doesn't have one.
51-
function defaultOnRejected(err: any) {
52-
log.error(err && err.message);
53-
}
54-
55-
function generateReadyPromise() {
56-
const promise = promiseWrapper(new Promise<void>((resolve, reject) => {
57-
readinessManager.gate.once(SDK_READY, () => {
58-
log.info(CLIENT_READY);
45+
readinessManager.gate.once(SDK_READY, () => {
46+
log.info(CLIENT_READY);
5947

60-
if (readyCbCount === internalReadyCbCount && !promise.hasOnFulfilled()) log.warn(CLIENT_NO_LISTENER);
61-
resolve();
62-
});
63-
readinessManager.gate.once(SDK_READY_TIMED_OUT, (message: string) => {
64-
reject(new Error(message));
65-
});
66-
}), defaultOnRejected);
48+
if (readyCbCount === internalReadyCbCount && !readyPromise) log.warn(CLIENT_NO_LISTENER);
49+
});
6750

68-
return promise;
69-
}
51+
readinessManager.gate.once(SDK_READY_TIMED_OUT, (message: string) => {
52+
log.error(message);
53+
});
7054

55+
readinessManager.gate.once(SDK_READY_FROM_CACHE, () => {
56+
log.info(CLIENT_READY_FROM_CACHE);
57+
});
7158

7259
return {
7360
readinessManager,
@@ -91,34 +78,19 @@ export function sdkReadinessManagerFactory(
9178
SDK_UPDATE,
9279
SDK_READY_TIMED_OUT,
9380
},
94-
/**
95-
* 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).
96-
* 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
97-
* SDK had timed out will return a new promise that should eventually resolve if the SDK gets ready.
98-
*
99-
* 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.
100-
* However, when using async/await syntax, the rejection should be explicitly propagated like in the following example:
101-
* ```
102-
* try {
103-
* await client.ready().catch((e) => { throw e; });
104-
* // SDK is ready
105-
* } catch(e) {
106-
* // SDK has timedout
107-
* }
108-
* ```
109-
*
110-
* @function ready
111-
* @returns {Promise<void>}
112-
*/
11381
ready() {
114-
if (readinessManager.hasTimedout()) {
115-
if (!readinessManager.isReady()) {
116-
return promiseWrapper(Promise.reject(new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.')), defaultOnRejected);
117-
} else {
118-
return Promise.resolve();
119-
}
120-
}
121-
return readyPromise;
82+
if (readinessManager.isReady()) return Promise.resolve();
83+
84+
if (readinessManager.hasTimedout()) return Promise.reject(new Error('Split SDK has emitted SDK_READY_TIMED_OUT event.'));
85+
86+
return readyPromise || (readyPromise = new Promise<void>((resolve, reject) => {
87+
readinessManager.gate.once(SDK_READY, () => {
88+
resolve();
89+
});
90+
readinessManager.gate.once(SDK_READY_TIMED_OUT, (message: string) => {
91+
reject(new Error(message));
92+
});
93+
}));
12294
},
12395

12496
__getStatus() {

src/types.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -390,20 +390,9 @@ export interface IStatusInterface extends IEventEmitter {
390390
*/
391391
Event: EventConsts,
392392
/**
393-
* 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).
394-
* 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
395-
* SDK had timed out will return a new promise that should eventually resolve if the SDK gets ready.
396-
*
397-
* 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.
398-
* However, when using async/await syntax, the rejection should be explicitly propagated like in the following example:
399-
* ```
400-
* try {
401-
* await client.ready().catch((e) => { throw e; });
402-
* // SDK is ready
403-
* } catch(e) {
404-
* // SDK has timedout
405-
* }
406-
* ```
393+
* 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).
394+
* 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.
395+
* 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.
407396
*
408397
* @function ready
409398
* @returns {Promise<void>}

src/utils/promise/__tests__/wrapper.spec.ts

Lines changed: 0 additions & 162 deletions
This file was deleted.

0 commit comments

Comments
 (0)