diff --git a/packages/sdk/browser/__tests__/BrowserClient.test.ts b/packages/sdk/browser/__tests__/BrowserClient.test.ts index af9b61c25..e2263f199 100644 --- a/packages/sdk/browser/__tests__/BrowserClient.test.ts +++ b/packages/sdk/browser/__tests__/BrowserClient.test.ts @@ -231,6 +231,102 @@ describe('given a mock platform for a BrowserClient', () => { expect(client.getContext()).toEqual({ kind: 'user', key: 'bob' }); }); + it('parses bootstrap data only once when using start()', async () => { + const bootstrapModule = await import('../src/bootstrap'); + const readFlagsFromBootstrapSpy = jest.spyOn(bootstrapModule, 'readFlagsFromBootstrap'); + + const client = makeClient( + 'client-side-id', + { kind: 'user', key: 'bob' }, + AutoEnvAttributes.Disabled, + { + streaming: false, + logger, + diagnosticOptOut: true, + }, + platform, + ); + + await client.start({ + identifyOptions: { + bootstrap: goodBootstrapDataWithReasons, + }, + }); + + expect(readFlagsFromBootstrapSpy).toHaveBeenCalledTimes(1); + expect(readFlagsFromBootstrapSpy).toHaveBeenCalledWith( + expect.anything(), + goodBootstrapDataWithReasons, + ); + + readFlagsFromBootstrapSpy.mockRestore(); + }); + + it('uses the latest bootstrap data when identify is called with new bootstrap data', async () => { + const initialBootstrapData = { + 'string-flag': 'is bob', + 'my-boolean-flag': false, + $flagsState: { + 'string-flag': { + variation: 1, + version: 3, + }, + 'my-boolean-flag': { + variation: 1, + version: 11, + }, + }, + $valid: true, + }; + + const newBootstrapData = { + 'string-flag': 'is alice', + 'my-boolean-flag': true, + $flagsState: { + 'string-flag': { + variation: 1, + version: 4, + }, + 'my-boolean-flag': { + variation: 0, + version: 12, + }, + }, + $valid: true, + }; + + const client = makeClient( + 'client-side-id', + { kind: 'user', key: 'bob' }, + AutoEnvAttributes.Disabled, + { + streaming: false, + logger, + diagnosticOptOut: true, + }, + platform, + ); + + await client.start({ + identifyOptions: { + bootstrap: initialBootstrapData, + }, + }); + + expect(client.stringVariation('string-flag', 'default')).toBe('is bob'); + expect(client.boolVariation('my-boolean-flag', false)).toBe(false); + + await client.identify( + { kind: 'user', key: 'alice' }, + { + bootstrap: newBootstrapData, + }, + ); + + expect(client.stringVariation('string-flag', 'default')).toBe('is alice'); + expect(client.boolVariation('my-boolean-flag', false)).toBe(true); + }); + it('can shed intermediate identify calls', async () => { const client = makeClient( 'client-side-id', diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 496e75e24..c16aecae9 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -276,8 +276,11 @@ class BrowserClientImpl extends LDClientImpl { if (identifyOptions?.bootstrap) { try { - const bootstrapData = readFlagsFromBootstrap(this.logger, identifyOptions.bootstrap); - this.presetFlags(bootstrapData); + identifyOptions.bootstrapParsed = readFlagsFromBootstrap( + this.logger, + identifyOptions.bootstrap, + ); + this.presetFlags(identifyOptions.bootstrapParsed); } catch (error) { this.logger.error('Failed to bootstrap data', error); } diff --git a/packages/sdk/browser/src/BrowserDataManager.ts b/packages/sdk/browser/src/BrowserDataManager.ts index ca3a8db41..2588aa0b7 100644 --- a/packages/sdk/browser/src/BrowserDataManager.ts +++ b/packages/sdk/browser/src/BrowserDataManager.ts @@ -4,6 +4,7 @@ import { Context, DataSourceErrorKind, DataSourcePaths, + DataSourceState, FlagManager, httpErrorMessage, internal, @@ -93,7 +94,7 @@ export default class BrowserDataManager extends BaseDataManager { this._secureModeHash = browserIdentifyOptions?.hash; if (browserIdentifyOptions?.bootstrap) { - this._finishIdentifyFromBootstrap(context, browserIdentifyOptions.bootstrap, identifyResolve); + this._finishIdentifyFromBootstrap(context, browserIdentifyOptions, identifyResolve); } else { if (await this.flagManager.loadCached(context)) { this._debugLog('Identify - Flags loaded from cache. Continuing to initialize via a poll.'); @@ -160,7 +161,7 @@ export default class BrowserDataManager extends BaseDataManager { identifyReject: (err: Error) => void, ) { try { - this.dataSourceStatusManager.requestStateUpdate('INITIALIZING'); + this.dataSourceStatusManager.requestStateUpdate(DataSourceState.Initializing); const payload = await this._requestPayload(context); @@ -186,10 +187,14 @@ export default class BrowserDataManager extends BaseDataManager { private _finishIdentifyFromBootstrap( context: Context, - bootstrap: unknown, + browserIdentifyOptions: BrowserIdentifyOptions, identifyResolve: () => void, ) { - this.flagManager.setBootstrap(context, readFlagsFromBootstrap(this.logger, bootstrap)); + let { bootstrapParsed } = browserIdentifyOptions; + if (!bootstrapParsed) { + bootstrapParsed = readFlagsFromBootstrap(this.logger, browserIdentifyOptions.bootstrap); + } + this.flagManager.setBootstrap(context, bootstrapParsed); this._debugLog('Identify - Initialization completed from bootstrap'); identifyResolve(); } diff --git a/packages/sdk/browser/src/BrowserIdentifyOptions.ts b/packages/sdk/browser/src/BrowserIdentifyOptions.ts index 224143487..13dabe168 100644 --- a/packages/sdk/browser/src/BrowserIdentifyOptions.ts +++ b/packages/sdk/browser/src/BrowserIdentifyOptions.ts @@ -1,4 +1,4 @@ -import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; +import { ItemDescriptor, LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common'; /** * @property sheddable - If true, the identify operation will be sheddable. This means that if multiple identify operations are done, without @@ -28,4 +28,12 @@ export interface BrowserIdentifyOptions extends Omit