Skip to content

Commit 78e9a5e

Browse files
feat: Add support for an identify queue. (#842)
Co-authored-by: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com>
1 parent 3b79457 commit 78e9a5e

File tree

16 files changed

+870
-191
lines changed

16 files changed

+870
-191
lines changed

actions/package-size/action.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ runs:
2828
run: |
2929
brotli ${{ inputs.target_file }}
3030
export PACK_SIZE=$(stat -c %s ${{ inputs.target_file }}.br)
31+
export RAW_SIZE=$(stat -c %s ${{ inputs.target_file }})
3132
echo "PACK_SIZE=$PACK_SIZE" >> $GITHUB_ENV
33+
echo "RAW_SIZE=$RAW_SIZE" >> $GITHUB_ENV
3234
3335
- name: Find Size Comment
3436
# Only do commenting on non-forks. A fork does not have permissions for comments.
@@ -48,8 +50,9 @@ runs:
4850
body: |
4951
${{ inputs.package_name }} size report
5052
This is the brotli compressed size of the ESM build.
51-
Size: ${{ env.PACK_SIZE }} bytes
52-
Size limit: ${{ inputs.size_limit }}
53+
Compressed size: ${{ env.PACK_SIZE }} bytes
54+
Compressed size limit: ${{ inputs.size_limit }}
55+
Uncompressed size: ${{ env.RAW_SIZE }} bytes
5356
5457
- name: Update comment
5558
if: steps.fc.outputs.comment-id != '' && github.event.pull_request.head.repo.full_name == github.repository
@@ -60,9 +63,9 @@ runs:
6063
body: |
6164
${{ inputs.package_name }} size report
6265
This is the brotli compressed size of the ESM build.
63-
Size: ${{ env.PACK_SIZE }} bytes
64-
Size limit: ${{ inputs.size_limit }}
65-
66+
Compressed size: ${{ env.PACK_SIZE }} bytes
67+
Compressed size limit: ${{ inputs.size_limit }}
68+
Uncompressed size: ${{ env.RAW_SIZE }} bytes
6669
- name: Check package size limit
6770
shell: bash
6871
run: |

packages/sdk/browser/__tests__/BrowserClient.test.ts

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { AutoEnvAttributes, LDLogger } from '@launchdarkly/js-client-sdk-common';
1+
import {
2+
AutoEnvAttributes,
3+
LDLogger,
4+
LDSingleKindContext,
5+
} from '@launchdarkly/js-client-sdk-common';
26

37
import { BrowserClient } from '../src/BrowserClient';
48
import { makeBasicPlatform } from './BrowserClient.mocks';
@@ -184,4 +188,198 @@ describe('given a mock platform for a BrowserClient', () => {
184188
variationIndex: 1,
185189
});
186190
});
191+
192+
it('can shed intermediate identifyResult calls', async () => {
193+
const client = new BrowserClient(
194+
'client-side-id',
195+
AutoEnvAttributes.Disabled,
196+
{ streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false },
197+
platform,
198+
);
199+
200+
const promise1 = client.identifyResult({ key: 'user-key-1', kind: 'user' });
201+
const promise2 = client.identifyResult({ key: 'user-key-2', kind: 'user' });
202+
const promise3 = client.identifyResult({ key: 'user-key-3', kind: 'user' });
203+
204+
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
205+
206+
expect(result1).toEqual({ status: 'completed' });
207+
expect(result2).toEqual({ status: 'shed' });
208+
expect(result3).toEqual({ status: 'completed' });
209+
// With events and goals disabled the only fetch calls should be for polling requests.
210+
expect(platform.requests.fetch.mock.calls.length).toBe(2);
211+
});
212+
213+
it('calls beforeIdentify in order', async () => {
214+
const order: string[] = [];
215+
const client = new BrowserClient(
216+
'client-side-id',
217+
AutoEnvAttributes.Disabled,
218+
{
219+
streaming: false,
220+
logger,
221+
diagnosticOptOut: true,
222+
sendEvents: false,
223+
fetchGoals: false,
224+
hooks: [
225+
{
226+
beforeIdentify: (hookContext, data) => {
227+
if ('kind' in hookContext.context && hookContext.context.kind !== 'multi') {
228+
order.push((hookContext.context as LDSingleKindContext).key);
229+
}
230+
231+
return data;
232+
},
233+
getMetadata: () => ({
234+
name: 'test-hook',
235+
version: '1.0.0',
236+
}),
237+
},
238+
],
239+
},
240+
platform,
241+
);
242+
243+
const promise1 = client.identify({ key: 'user-key-1', kind: 'user' });
244+
const promise2 = client.identify({ key: 'user-key-2', kind: 'user' });
245+
const promise3 = client.identify({ key: 'user-key-3', kind: 'user' });
246+
247+
await Promise.all([promise1, promise2, promise3]);
248+
expect(order).toEqual(['user-key-1', 'user-key-2', 'user-key-3']);
249+
});
250+
251+
it('completes identify calls in order', async () => {
252+
const order: string[] = [];
253+
const client = new BrowserClient(
254+
'client-side-id',
255+
AutoEnvAttributes.Disabled,
256+
{
257+
streaming: false,
258+
logger,
259+
diagnosticOptOut: true,
260+
sendEvents: false,
261+
fetchGoals: false,
262+
hooks: [
263+
{
264+
afterIdentify: (hookContext, data, result) => {
265+
if (result.status === 'shed') {
266+
return data;
267+
}
268+
if ('kind' in hookContext.context && hookContext.context.kind !== 'multi') {
269+
order.push((hookContext.context as LDSingleKindContext).key);
270+
}
271+
272+
return data;
273+
},
274+
getMetadata: () => ({
275+
name: 'test-hook',
276+
version: '1.0.0',
277+
}),
278+
},
279+
],
280+
},
281+
platform,
282+
);
283+
284+
const promise1 = client.identify({ key: 'user-key-1', kind: 'user' });
285+
const promise2 = client.identify({ key: 'user-key-2', kind: 'user' });
286+
const promise3 = client.identify({ key: 'user-key-3', kind: 'user' });
287+
288+
await Promise.all([promise1, promise2, promise3]);
289+
// user-key-2 is shed, so it is not included in the order
290+
expect(order).toEqual(['user-key-1', 'user-key-3']);
291+
});
292+
293+
it('completes awaited identify calls in order without shedding', async () => {
294+
const order: string[] = [];
295+
const client = new BrowserClient(
296+
'client-side-id',
297+
AutoEnvAttributes.Disabled,
298+
{
299+
streaming: false,
300+
logger,
301+
diagnosticOptOut: true,
302+
sendEvents: false,
303+
fetchGoals: false,
304+
hooks: [
305+
{
306+
afterIdentify: (hookContext, data, result) => {
307+
if (result.status === 'shed') {
308+
return data;
309+
}
310+
if ('kind' in hookContext.context && hookContext.context.kind !== 'multi') {
311+
order.push((hookContext.context as LDSingleKindContext).key);
312+
}
313+
314+
return data;
315+
},
316+
getMetadata: () => ({
317+
name: 'test-hook',
318+
version: '1.0.0',
319+
}),
320+
},
321+
],
322+
},
323+
platform,
324+
);
325+
326+
const result1 = await client.identifyResult({ key: 'user-key-1', kind: 'user' });
327+
const result2 = await client.identifyResult({ key: 'user-key-2', kind: 'user' });
328+
const result3 = await client.identifyResult({ key: 'user-key-3', kind: 'user' });
329+
330+
expect(result1.status).toEqual('completed');
331+
expect(result2.status).toEqual('completed');
332+
expect(result3.status).toEqual('completed');
333+
334+
// user-key-2 is shed, so it is not included in the order
335+
expect(order).toEqual(['user-key-1', 'user-key-2', 'user-key-3']);
336+
});
337+
338+
it('can shed intermediate identify calls', async () => {
339+
const client = new BrowserClient(
340+
'client-side-id',
341+
AutoEnvAttributes.Disabled,
342+
{ streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false },
343+
platform,
344+
);
345+
346+
const promise1 = client.identify({ key: 'user-key-1', kind: 'user' });
347+
const promise2 = client.identify({ key: 'user-key-2', kind: 'user' });
348+
const promise3 = client.identify({ key: 'user-key-3', kind: 'user' });
349+
350+
await Promise.all([promise1, promise2, promise3]);
351+
352+
// With events and goals disabled the only fetch calls should be for polling requests.
353+
expect(platform.requests.fetch.mock.calls.length).toBe(2);
354+
});
355+
356+
it('it does not shed non-shedable identify calls', async () => {
357+
const client = new BrowserClient(
358+
'client-side-id',
359+
AutoEnvAttributes.Disabled,
360+
{ streaming: false, logger, diagnosticOptOut: true, sendEvents: false, fetchGoals: false },
361+
platform,
362+
);
363+
364+
const promise1 = client.identifyResult(
365+
{ key: 'user-key-1', kind: 'user' },
366+
{ sheddable: false },
367+
);
368+
const promise2 = client.identifyResult(
369+
{ key: 'user-key-2', kind: 'user' },
370+
{ sheddable: false },
371+
);
372+
const promise3 = client.identifyResult(
373+
{ key: 'user-key-3', kind: 'user' },
374+
{ sheddable: false },
375+
);
376+
377+
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
378+
379+
expect(result1).toEqual({ status: 'completed' });
380+
expect(result2).toEqual({ status: 'completed' });
381+
expect(result3).toEqual({ status: 'completed' });
382+
// With events and goals disabled the only fetch calls should be for polling requests.
383+
expect(platform.requests.fetch.mock.calls.length).toBe(3);
384+
});
187385
});

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
LDEmitter,
1212
LDEmitterEventName,
1313
LDHeaders,
14+
LDIdentifyResult,
1415
LDPluginEnvironmentMetadata,
1516
Platform,
1617
} from '@launchdarkly/js-client-sdk-common';
@@ -188,8 +189,22 @@ export class BrowserClient extends LDClientImpl implements LDClient {
188189
}
189190

190191
override async identify(context: LDContext, identifyOptions?: LDIdentifyOptions): Promise<void> {
191-
await super.identify(context, identifyOptions);
192+
return super.identify(context, identifyOptions);
193+
}
194+
195+
override async identifyResult(
196+
context: LDContext,
197+
identifyOptions?: LDIdentifyOptions,
198+
): Promise<LDIdentifyResult> {
199+
const identifyOptionsWithUpdatedDefaults = {
200+
...identifyOptions,
201+
};
202+
if (identifyOptions?.sheddable === undefined) {
203+
identifyOptionsWithUpdatedDefaults.sheddable = true;
204+
}
205+
const res = await super.identifyResult(context, identifyOptionsWithUpdatedDefaults);
192206
this._goalManager?.startTracking();
207+
return res;
193208
}
194209

195210
setStreaming(streaming?: boolean): void {

packages/sdk/browser/src/BrowserIdentifyOptions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common';
22

3+
/**
4+
* @property sheddable - If true, the identify operation will be sheddable. This means that if multiple identify operations are done, without
5+
* waiting for the previous one to complete, then intermediate results will be discarded. When false, identify
6+
* operations will be queued and completed sequentially.
7+
*
8+
* Defaults to true.
9+
*/
310
export interface BrowserIdentifyOptions extends Omit<LDIdentifyOptions, 'waitForNetworkResults'> {
411
/**
512
* The signed context key if you are using [Secure Mode]

packages/sdk/browser/src/LDClient.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { LDClient as CommonClient, LDContext } from '@launchdarkly/js-client-sdk-common';
1+
import {
2+
LDClient as CommonClient,
3+
LDContext,
4+
LDIdentifyResult,
5+
} from '@launchdarkly/js-client-sdk-common';
26

37
import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOptions';
48

@@ -34,7 +38,8 @@ export type LDClient = Omit<
3438
setStreaming(streaming?: boolean): void;
3539

3640
/**
37-
* Identifies a context to LaunchDarkly.
41+
* Identifies a context to LaunchDarkly and returns a promise which resolves to an object containing the result of
42+
* the identify operation.
3843
*
3944
* Unlike the server-side SDKs, the client-side JavaScript SDKs maintain a current context state,
4045
* which is set when you call `identify()`.
@@ -43,6 +48,10 @@ export type LDClient = Omit<
4348
* finished, calls to {@link variation} will still return flag values for the previous context. You can
4449
* await the Promise to determine when the new flag values are available.
4550
*
51+
* This function will shed intermediate identify operations by default. For example, if you call identify 3 times in
52+
* a row, without waiting for the previous one to complete, the middle call to identify may be discarded. To disable
53+
* this set `sheddable` to `false` in the `identifyOptions` parameter.
54+
*
4655
* @param context
4756
* The LDContext object.
4857
* @param identifyOptions
@@ -61,4 +70,34 @@ export type LDClient = Omit<
6170
* @ignore Implementation Note: Browser implementation has different options.
6271
*/
6372
identify(context: LDContext, identifyOptions?: LDIdentifyOptions): Promise<void>;
73+
74+
/**
75+
* Identifies a context to LaunchDarkly and returns a promise which resolves to an object containing the result of
76+
* the identify operation.
77+
*
78+
* Unlike the server-side SDKs, the client-side JavaScript SDKs maintain a current context state,
79+
* which is set when you call `identify()`.
80+
*
81+
* Changing the current context also causes all feature flag values to be reloaded. Until that has
82+
* finished, calls to {@link variation} will still return flag values for the previous context. You can
83+
* await the Promise to determine when the new flag values are available.
84+
*
85+
* If used with the `sheddable` option set to true, then the identify operation will be sheddable. This means that if
86+
* multiple identify operations are done, without waiting for the previous one to complete, then intermediate
87+
* operations may be discarded.
88+
*
89+
* @param context
90+
* The LDContext object.
91+
* @param identifyOptions
92+
* Optional configuration. Please see {@link LDIdentifyOptions}.
93+
* @returns
94+
* A promise which resolves to an object containing the result of the identify operation.
95+
* The promise returned from this method will not be rejected.
96+
*
97+
* @ignore Implementation Note: Browser implementation has different options.
98+
*/
99+
identifyResult(
100+
pristineContext: LDContext,
101+
identifyOptions?: LDIdentifyOptions,
102+
): Promise<LDIdentifyResult>;
64103
};

packages/sdk/browser/src/common.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ export type {
3838
LDPluginSdkMetadata,
3939
LDPluginApplicationMetadata,
4040
LDPluginMetadata,
41+
LDIdentifyResult,
42+
LDIdentifySuccess,
43+
LDIdentifyError,
44+
LDIdentifyTimeout,
45+
LDIdentifyShed,
4146
} from '@launchdarkly/js-client-sdk-common';
4247

4348
/**

packages/sdk/browser/src/compat/LDClientCompat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { LDClient as LDCLientBrowser } from '../LDClient';
1212
* incorrect. Any function which optionally returned a promise based on a callback had incorrect
1313
* typings. Those have been corrected in this implementation.
1414
*/
15-
export interface LDClient extends Omit<LDCLientBrowser, 'close' | 'flush' | 'identify'> {
15+
export interface LDClient
16+
extends Omit<LDCLientBrowser, 'close' | 'flush' | 'identify' | 'identifyResult'> {
1617
/**
1718
* Identifies a context to LaunchDarkly.
1819
*

0 commit comments

Comments
 (0)