Skip to content

Commit 9a8ee5b

Browse files
committed
feat: adding waitForInitialize to browser 4.x
1 parent 5308cfd commit 9a8ee5b

File tree

4 files changed

+96
-1
lines changed

4 files changed

+96
-1
lines changed

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
AutoEnvAttributes,
33
base64UrlEncode,
44
BasicLogger,
5+
cancelableTimedPromise,
56
Configuration,
67
Encoding,
78
FlagManager,
@@ -15,6 +16,7 @@ import {
1516
LDHeaders,
1617
LDIdentifyResult,
1718
LDPluginEnvironmentMetadata,
19+
LDTimeoutError,
1820
Platform,
1921
} from '@launchdarkly/js-client-sdk-common';
2022

@@ -32,6 +34,9 @@ import BrowserPlatform from './platform/BrowserPlatform';
3234
class BrowserClientImpl extends LDClientImpl {
3335
private readonly _goalManager?: GoalManager;
3436
private readonly _plugins?: LDPlugin[];
37+
private _initializedPromise?: Promise<void>;
38+
private _initResolve?: () => void;
39+
private _isInitialized: boolean = false;
3540

3641
constructor(
3742
clientSideId: string,
@@ -212,10 +217,60 @@ class BrowserClientImpl extends LDClientImpl {
212217
identifyOptionsWithUpdatedDefaults.sheddable = true;
213218
}
214219
const res = await super.identifyResult(context, identifyOptionsWithUpdatedDefaults);
220+
if (res.status === 'completed') {
221+
this._isInitialized = true;
222+
this._initResolve?.();
223+
}
215224
this._goalManager?.startTracking();
216225
return res;
217226
}
218227

228+
waitForInitialization(timeout: number = 5): Promise<void> {
229+
// An initialization promise is only created if someone is going to use that promise.
230+
// If we always created an initialization promise, and there was no call waitForInitialization
231+
// by the time the promise was rejected, then that would result in an unhandled promise
232+
// rejection.
233+
234+
// It waitForInitialization was previously called, then we can use that promise even if it has
235+
// been resolved or rejected.
236+
if (this._initializedPromise) {
237+
return this._promiseWithTimeout(this._initializedPromise, timeout);
238+
}
239+
240+
if (this._isInitialized) {
241+
return Promise.resolve();
242+
}
243+
244+
if (!this._initializedPromise) {
245+
this._initializedPromise = new Promise((resolve) => {
246+
this._initResolve = resolve;
247+
});
248+
}
249+
250+
return this._promiseWithTimeout(this._initializedPromise, timeout);
251+
}
252+
253+
/**
254+
* Apply a timeout promise to a base promise. This is for use with waitForInitialization.
255+
*
256+
* @param basePromise The promise to race against a timeout.
257+
* @param timeout The timeout in seconds.
258+
* @param logger A logger to log when the timeout expires.
259+
* @returns
260+
*/
261+
private _promiseWithTimeout(basePromise: Promise<void>, timeout: number): Promise<void> {
262+
const cancelableTimeout = cancelableTimedPromise(timeout, 'waitForInitialization');
263+
return Promise.race([
264+
basePromise.then(() => cancelableTimeout.cancel()),
265+
cancelableTimeout.promise,
266+
]).catch((reason) => {
267+
if (reason instanceof LDTimeoutError) {
268+
this.logger?.error(reason.message);
269+
}
270+
throw reason;
271+
});
272+
}
273+
219274
setStreaming(streaming?: boolean): void {
220275
// With FDv2 we may want to consider if we support connection mode directly.
221276
// Maybe with an extension to connection mode for 'automatic'.
@@ -282,6 +337,7 @@ export function makeClient(
282337
close: () => impl.close(),
283338
allFlags: () => impl.allFlags(),
284339
addHook: (hook: Hook) => impl.addHook(hook),
340+
waitForInitialization: (timeout: number) => impl.waitForInitialization(timeout),
285341
logger: impl.logger,
286342
};
287343

packages/sdk/browser/src/LDClient.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,42 @@ export type LDClient = Omit<
6666
pristineContext: LDContext,
6767
identifyOptions?: LDIdentifyOptions,
6868
): Promise<LDIdentifyResult>;
69+
70+
/**
71+
* Returns a Promise that tracks the client's initialization state.
72+
*
73+
* The Promise will be resolved if the client successfully initializes, or rejected if client
74+
* initialization takes longer than the set timeout.
75+
*
76+
* ```
77+
* // using async/await
78+
* try {
79+
* await client.waitForInitialization(5);
80+
* doSomethingWithSuccessfullyInitializedClient();
81+
* } catch (err) {
82+
* doSomethingForFailedStartup(err);
83+
* }
84+
* ```
85+
*
86+
* It is important that you handle the rejection case; otherwise it will become an unhandled Promise
87+
* rejection, which is a serious error on some platforms. The Promise is not created unless you
88+
* request it, so if you never call `waitForInitialization()` then you do not have to worry about
89+
* unhandled rejections.
90+
*
91+
* Note that you can also use event listeners ({@link on}) for the same purpose: the event `"initialized"`
92+
* indicates success, and `"error"` indicates an error.
93+
*
94+
* @param timeout
95+
* The amount of time, in seconds, to wait for initialization before rejecting the promise.
96+
* Using a large timeout is not recommended. If you use a large timeout and await it, then
97+
* any network delays will cause your application to wait a long time before
98+
* continuing execution.
99+
*
100+
* @default 5 seconds
101+
*
102+
* @returns
103+
* A Promise that will be resolved if the client initializes successfully, or rejected if it
104+
* fails or the specified timeout elapses.
105+
*/
106+
waitForInitialization(timeout?: number): Promise<void>;
69107
};

packages/shared/sdk-client/src/LDClientImpl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
345345
if (res.status === 'shed') {
346346
return { status: 'shed' } as LDIdentifyShed;
347347
}
348+
this.emitter.emit('initialized');
348349
return { status: 'completed' } as LDIdentifySuccess;
349350
});
350351

packages/shared/sdk-client/src/LDEmitter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ type FlagChangeKey = `change:${string}`;
66
* Type for name of emitted events. 'change' is used for all flag changes. 'change:flag-name-here' is used
77
* for specific flag changes.
88
*/
9-
export type EventName = 'change' | FlagChangeKey | 'dataSourceStatus' | 'error';
9+
export type EventName = 'change' | FlagChangeKey | 'dataSourceStatus' | 'error' | 'initialized';
1010

1111
/**
1212
* Implementation Note: There should not be any default listeners for change events in a client

0 commit comments

Comments
 (0)