Skip to content

Commit 1375660

Browse files
feat: Allow waiting for the network response on identify. (#548)
Fixes: #503 Fixes: #505 When non-cached values are desired after identify the `waitForNetworkResults` identify option can be used. This will caused the identify to wait for configuration from the configured data source or timeout. The recommended approach, for resilience and performance, will be to use cached flag values and respond to fresh values reactively and that will remain the default. --------- Co-authored-by: Todd Anderson <[email protected]>
1 parent ab05253 commit 1375660

File tree

3 files changed

+79
-15
lines changed

3 files changed

+79
-15
lines changed

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

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,6 @@ import * as mockResponseJson from './evaluation/mockResponse.json';
1010
import LDClientImpl from './LDClientImpl';
1111
import { Flags } from './types';
1212

13-
let mockPlatform: ReturnType<typeof createBasicPlatform>;
14-
let logger: ReturnType<typeof createLogger>;
15-
16-
beforeEach(() => {
17-
mockPlatform = createBasicPlatform();
18-
logger = createLogger();
19-
});
20-
2113
jest.mock('@launchdarkly/js-sdk-common', () => {
2214
const actual = jest.requireActual('@launchdarkly/js-sdk-common');
2315
const actualMock = jest.requireActual('@launchdarkly/private-js-mocks');
@@ -49,11 +41,15 @@ const autoEnv = {
4941
os: { name: 'An OS', version: '1.0.1', family: 'orange' },
5042
},
5143
};
52-
let ldc: LDClientImpl;
53-
let defaultPutResponse: Flags;
54-
5544
describe('sdk-client object', () => {
45+
let ldc: LDClientImpl;
46+
let defaultPutResponse: Flags;
47+
let mockPlatform: ReturnType<typeof createBasicPlatform>;
48+
let logger: ReturnType<typeof createLogger>;
49+
5650
beforeEach(() => {
51+
mockPlatform = createBasicPlatform();
52+
logger = createLogger();
5753
defaultPutResponse = clone<Flags>(mockResponseJson);
5854
setupMockStreamingProcessor(false, defaultPutResponse);
5955
mockPlatform.crypto.randomUUID.mockReturnValue('random1');
@@ -97,6 +93,8 @@ describe('sdk-client object', () => {
9793
defaultPutResponse['dev-test-flag'].value = false;
9894
const carContext: LDContext = { kind: 'car', key: 'test-car' };
9995

96+
mockPlatform.crypto.randomUUID.mockReturnValue('random1');
97+
10098
await ldc.identify(carContext);
10199
const c = ldc.getContext();
102100
const all = ldc.allFlags();
@@ -161,6 +159,8 @@ describe('sdk-client object', () => {
161159
defaultPutResponse['dev-test-flag'].value = false;
162160
const carContext: LDContext = { kind: 'car', anonymous: true, key: '' };
163161

162+
mockPlatform.crypto.randomUUID.mockReturnValue('random1');
163+
164164
await ldc.identify(carContext);
165165
const c = ldc.getContext();
166166
const all = ldc.allFlags();
@@ -207,4 +207,46 @@ describe('sdk-client object', () => {
207207
expect(emitter.listenerCount('change')).toEqual(1);
208208
expect(emitter.listenerCount('error')).toEqual(1);
209209
});
210+
211+
test('can complete identification using storage', async () => {
212+
const data: Record<string, string> = {};
213+
mockPlatform.storage.get.mockImplementation((key) => data[key]);
214+
mockPlatform.storage.set.mockImplementation((key: string, value: string) => {
215+
data[key] = value;
216+
});
217+
mockPlatform.storage.clear.mockImplementation((key: string) => {
218+
delete data[key];
219+
});
220+
221+
// First identify should populate storage.
222+
await ldc.identify(context);
223+
224+
expect(logger.debug).not.toHaveBeenCalledWith('Identify completing with cached flags');
225+
226+
// Second identify should use storage.
227+
await ldc.identify(context);
228+
229+
expect(logger.debug).toHaveBeenCalledWith('Identify completing with cached flags');
230+
});
231+
232+
test('does not complete identify using storage when instructed to wait for the network response', async () => {
233+
const data: Record<string, string> = {};
234+
mockPlatform.storage.get.mockImplementation((key) => data[key]);
235+
mockPlatform.storage.set.mockImplementation((key: string, value: string) => {
236+
data[key] = value;
237+
});
238+
mockPlatform.storage.clear.mockImplementation((key: string) => {
239+
delete data[key];
240+
});
241+
242+
// First identify should populate storage.
243+
await ldc.identify(context);
244+
245+
expect(logger.debug).not.toHaveBeenCalledWith('Identify completing with cached flags');
246+
247+
// Second identify would use storage, but we instruct it not to.
248+
await ldc.identify(context, { waitForNetworkResults: true, timeout: 5 });
249+
250+
expect(logger.debug).not.toHaveBeenCalledWith('Identify completing with cached flags');
251+
});
210252
});

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ export default class LDClientImpl implements LDClient {
217217
},
218218
{},
219219
);
220-
221220
await this.flagManager.init(context, descriptors).then(identifyResolve());
222221
},
223222
});
@@ -320,6 +319,9 @@ export default class LDClientImpl implements LDClient {
320319
* 3. A network error is encountered during initialization.
321320
*/
322321
async identify(pristineContext: LDContext, identifyOptions?: LDIdentifyOptions): Promise<void> {
322+
// In offline mode we do not support waiting for results.
323+
const waitForNetworkResults = !!identifyOptions?.waitForNetworkResults && !this.isOffline();
324+
323325
if (identifyOptions?.timeout) {
324326
this.identifyTimeout = identifyOptions.timeout;
325327
}
@@ -354,15 +356,23 @@ export default class LDClientImpl implements LDClient {
354356
this.logger.debug(`Identifying ${JSON.stringify(this.checkedContext)}`);
355357

356358
const loadedFromCache = await this.flagManager.loadCached(this.checkedContext);
357-
if (loadedFromCache) {
359+
if (loadedFromCache && !waitForNetworkResults) {
360+
this.logger.debug('Identify completing with cached flags');
358361
identifyResolve();
359362
}
363+
if (loadedFromCache && waitForNetworkResults) {
364+
this.logger.debug(
365+
'Identify - Flags loaded from cache, but identify was requested with "waitForNetworkResults"',
366+
);
367+
}
360368

361369
if (this.isOffline()) {
362370
if (loadedFromCache) {
363-
this.logger.debug('Offline identify using storage flags.');
371+
this.logger.debug('Offline identify - using cached flags.');
364372
} else {
365-
this.logger.debug('Offline identify no storage. Defaults will be used.');
373+
this.logger.debug(
374+
'Offline identify - no cached flags, using defaults or already loaded flags.',
375+
);
366376
identifyResolve();
367377
}
368378
} else {

packages/shared/sdk-client/src/api/LDIdentifyOptions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,16 @@ export interface LDIdentifyOptions {
88
* Defaults to 5 seconds.
99
*/
1010
timeout: number;
11+
12+
/**
13+
* When true indicates that the SDK will attempt to wait for values from
14+
* LaunchDarkly instead of depending on cached values. The cached values will
15+
* still be loaded, but the promise returned by the identify function will not
16+
* resolve as a result of those cached values being loaded. Generally this
17+
* option should NOT be used and instead flag changes should be listened to.
18+
* If the client is set to offline mode, then this option is ignored.
19+
*
20+
* Defaults to false.
21+
*/
22+
waitForNetworkResults?: boolean;
1123
}

0 commit comments

Comments
 (0)