diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/src/LDClientImpl.test.ts index dc2319c2b9..6fd145f617 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.test.ts @@ -10,14 +10,6 @@ import * as mockResponseJson from './evaluation/mockResponse.json'; import LDClientImpl from './LDClientImpl'; import { Flags } from './types'; -let mockPlatform: ReturnType; -let logger: ReturnType; - -beforeEach(() => { - mockPlatform = createBasicPlatform(); - logger = createLogger(); -}); - jest.mock('@launchdarkly/js-sdk-common', () => { const actual = jest.requireActual('@launchdarkly/js-sdk-common'); const actualMock = jest.requireActual('@launchdarkly/private-js-mocks'); @@ -49,11 +41,15 @@ const autoEnv = { os: { name: 'An OS', version: '1.0.1', family: 'orange' }, }, }; -let ldc: LDClientImpl; -let defaultPutResponse: Flags; - describe('sdk-client object', () => { + let ldc: LDClientImpl; + let defaultPutResponse: Flags; + let mockPlatform: ReturnType; + let logger: ReturnType; + beforeEach(() => { + mockPlatform = createBasicPlatform(); + logger = createLogger(); defaultPutResponse = clone(mockResponseJson); setupMockStreamingProcessor(false, defaultPutResponse); mockPlatform.crypto.randomUUID.mockReturnValue('random1'); @@ -97,6 +93,8 @@ describe('sdk-client object', () => { defaultPutResponse['dev-test-flag'].value = false; const carContext: LDContext = { kind: 'car', key: 'test-car' }; + mockPlatform.crypto.randomUUID.mockReturnValue('random1'); + await ldc.identify(carContext); const c = ldc.getContext(); const all = ldc.allFlags(); @@ -161,6 +159,8 @@ describe('sdk-client object', () => { defaultPutResponse['dev-test-flag'].value = false; const carContext: LDContext = { kind: 'car', anonymous: true, key: '' }; + mockPlatform.crypto.randomUUID.mockReturnValue('random1'); + await ldc.identify(carContext); const c = ldc.getContext(); const all = ldc.allFlags(); @@ -207,4 +207,46 @@ describe('sdk-client object', () => { expect(emitter.listenerCount('change')).toEqual(1); expect(emitter.listenerCount('error')).toEqual(1); }); + + test('can complete identification using storage', async () => { + const data: Record = {}; + mockPlatform.storage.get.mockImplementation((key) => data[key]); + mockPlatform.storage.set.mockImplementation((key: string, value: string) => { + data[key] = value; + }); + mockPlatform.storage.clear.mockImplementation((key: string) => { + delete data[key]; + }); + + // First identify should populate storage. + await ldc.identify(context); + + expect(logger.debug).not.toHaveBeenCalledWith('Identify completing with cached flags'); + + // Second identify should use storage. + await ldc.identify(context); + + expect(logger.debug).toHaveBeenCalledWith('Identify completing with cached flags'); + }); + + test('does not complete identify using storage when instructed to wait for the network response', async () => { + const data: Record = {}; + mockPlatform.storage.get.mockImplementation((key) => data[key]); + mockPlatform.storage.set.mockImplementation((key: string, value: string) => { + data[key] = value; + }); + mockPlatform.storage.clear.mockImplementation((key: string) => { + delete data[key]; + }); + + // First identify should populate storage. + await ldc.identify(context); + + expect(logger.debug).not.toHaveBeenCalledWith('Identify completing with cached flags'); + + // Second identify would use storage, but we instruct it not to. + await ldc.identify(context, { waitForNetworkResults: true, timeout: 5 }); + + expect(logger.debug).not.toHaveBeenCalledWith('Identify completing with cached flags'); + }); }); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 7c087379b5..78d8b3de86 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -217,7 +217,6 @@ export default class LDClientImpl implements LDClient { }, {}, ); - await this.flagManager.init(context, descriptors).then(identifyResolve()); }, }); @@ -320,6 +319,9 @@ export default class LDClientImpl implements LDClient { * 3. A network error is encountered during initialization. */ async identify(pristineContext: LDContext, identifyOptions?: LDIdentifyOptions): Promise { + // In offline mode we do not support waiting for results. + const waitForNetworkResults = !!identifyOptions?.waitForNetworkResults && !this.isOffline(); + if (identifyOptions?.timeout) { this.identifyTimeout = identifyOptions.timeout; } @@ -354,15 +356,23 @@ export default class LDClientImpl implements LDClient { this.logger.debug(`Identifying ${JSON.stringify(this.checkedContext)}`); const loadedFromCache = await this.flagManager.loadCached(this.checkedContext); - if (loadedFromCache) { + if (loadedFromCache && !waitForNetworkResults) { + this.logger.debug('Identify completing with cached flags'); identifyResolve(); } + if (loadedFromCache && waitForNetworkResults) { + this.logger.debug( + 'Identify - Flags loaded from cache, but identify was requested with "waitForNetworkResults"', + ); + } if (this.isOffline()) { if (loadedFromCache) { - this.logger.debug('Offline identify using storage flags.'); + this.logger.debug('Offline identify - using cached flags.'); } else { - this.logger.debug('Offline identify no storage. Defaults will be used.'); + this.logger.debug( + 'Offline identify - no cached flags, using defaults or already loaded flags.', + ); identifyResolve(); } } else { diff --git a/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts b/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts index 355c0e93ec..d9d33213e1 100644 --- a/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts +++ b/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts @@ -8,4 +8,16 @@ export interface LDIdentifyOptions { * Defaults to 5 seconds. */ timeout: number; + + /** + * When true indicates that the SDK will attempt to wait for values from + * LaunchDarkly instead of depending on cached values. The cached values will + * still be loaded, but the promise returned by the identify function will not + * resolve as a result of those cached values being loaded. Generally this + * option should NOT be used and instead flag changes should be listened to. + * If the client is set to offline mode, then this option is ignored. + * + * Defaults to false. + */ + waitForNetworkResults?: boolean; }