Skip to content

Commit 4abd3fe

Browse files
authored
Merge branch 'christie/sc-161963/flags-proxy-third-times-the-charm' into christie/sc-164657/no-variation-call-on-init
2 parents f1253c0 + 06ad141 commit 4abd3fe

File tree

11 files changed

+60
-70
lines changed

11 files changed

+60
-70
lines changed

src/__snapshots__/provider.test.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ exports[`LDProvider render app 1`] = `
44
<Provider
55
value={
66
Object {
7-
"_flags": Object {},
87
"flagKeyMap": Object {},
98
"flags": Object {},
109
"ldClient": undefined,

src/__snapshots__/withLDProvider.test.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ exports[`withLDProvider render app 1`] = `
44
<Provider
55
value={
66
Object {
7-
"_flags": Object {},
87
"flagKeyMap": Object {},
98
"flags": Object {},
109
"ldClient": undefined,

src/asyncWithLDProvider.test.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import React from 'react';
1212
import { render } from '@testing-library/react';
1313
import { LDFlagChangeset, LDOptions, LDUser } from 'launchdarkly-js-client-sdk';
1414
import initLDClient from './initLDClient';
15-
import { fetchFlags } from './utils';
1615
import { AsyncProviderConfig, LDReactOptions } from './types';
1716
import { Consumer } from './context';
1817
import asyncWithLDProvider from './asyncWithLDProvider';
@@ -21,8 +20,6 @@ const clientSideID = 'deadbeef';
2120
const user: LDUser = { key: 'yus', name: 'yus ng' };
2221
const App = () => <>My App</>;
2322
const mockInitLDClient = initLDClient as jest.Mock;
24-
const mockFetchFlags = fetchFlags as jest.Mock;
25-
const mockFlags = { testFlag: true, anotherTestFlag: true };
2623
const rawFlags = { 'test-flag': true, 'another-test-flag': true };
2724
let mockLDClient: { on: jest.Mock; off: jest.Mock; variation: jest.Mock };
2825

@@ -53,8 +50,6 @@ describe('asyncWithLDProvider', () => {
5350
ldClient: mockLDClient,
5451
flags: rawFlags,
5552
}));
56-
57-
mockFetchFlags.mockReturnValue(mockFlags);
5853
});
5954

6055
afterEach(() => {
@@ -102,7 +97,6 @@ describe('asyncWithLDProvider', () => {
10297
});
10398

10499
test('subscribe to changes with kebab-case', async () => {
105-
mockFetchFlags.mockReturnValue({ 'another-test-flag': true, 'test-flag': true });
106100
mockInitLDClient.mockImplementation(() => ({
107101
ldClient: mockLDClient,
108102
flags: rawFlags,

src/asyncWithLDProvider.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect, ReactNode } from 'react';
2-
import { LDFlagChangeset } from 'launchdarkly-js-client-sdk';
2+
import { LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk';
33
import { AsyncProviderConfig, defaultReactOptions } from './types';
44
import { Provider } from './context';
55
import initLDClient from './initLDClient';
@@ -38,21 +38,26 @@ export default async function asyncWithLDProvider(config: AsyncProviderConfig) {
3838
const LDProvider = ({ children }: { children: ReactNode }) => {
3939
const [ldData, setLDData] = useState({
4040
flags: {},
41-
_flags: {},
41+
unproxiedFlags: {},
4242
flagKeyMap: {},
4343
});
4444

4545
useEffect(() => {
46-
if (options?.bootstrap && options.bootstrap !== 'localStorage') {
47-
setLDData(getFlagsProxy(ldClient, options.bootstrap, reactOptions, targetFlags));
48-
} else {
49-
setLDData(getFlagsProxy(ldClient, fetchedFlags, reactOptions, targetFlags));
50-
}
46+
const initialFlags =
47+
options?.bootstrap && options.bootstrap !== 'localStorage' ? options.bootstrap : fetchedFlags;
48+
setLDData({ unproxiedFlags: initialFlags, ...getFlagsProxy(ldClient, initialFlags, reactOptions, targetFlags) });
5149

5250
function onChange(changes: LDFlagChangeset) {
5351
const updates = getFlattenedFlagsFromChangeset(changes, targetFlags);
5452
if (Object.keys(updates).length > 0) {
55-
setLDData(({ _flags }) => getFlagsProxy(ldClient, { ..._flags, ...updates }, reactOptions, targetFlags));
53+
setLDData(({ unproxiedFlags }) => {
54+
const updatedUnproxiedFlags = { ...unproxiedFlags, ...updates };
55+
56+
return {
57+
unproxiedFlags: updatedUnproxiedFlags,
58+
...getFlagsProxy(ldClient, updatedUnproxiedFlags, reactOptions, targetFlags),
59+
};
60+
});
5661
}
5762
}
5863
ldClient.on('change', onChange);
@@ -62,7 +67,9 @@ export default async function asyncWithLDProvider(config: AsyncProviderConfig) {
6267
};
6368
}, []);
6469

65-
return <Provider value={{ ...ldData, ldClient }}>{children}</Provider>;
70+
const { flags, flagKeyMap } = ldData;
71+
72+
return <Provider value={{ flags, flagKeyMap, ldClient }}>{children}</Provider>;
6673
};
6774

6875
return LDProvider;

src/context.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ interface LDContext {
1313
*/
1414
flags: LDFlagSet;
1515

16-
/**
17-
* Un-proxied un-camelized copy of flags.
18-
*/
19-
_flags: LDFlagSet;
20-
2116
/**
2217
* Map of camelized flag keys to their original unmodified form. Empty if camelization option is off.
2318
*/
@@ -35,7 +30,7 @@ interface LDContext {
3530
/**
3631
* @ignore
3732
*/
38-
const context = createContext<LDContext>({ flags: {}, _flags: {}, flagKeyMap: {} });
33+
const context = createContext<LDContext>({ flags: {}, flagKeyMap: {} });
3934
const {
4035
/**
4136
* @ignore

src/getFlagsProxy.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function getFlagsProxy(
77
rawFlags: LDFlagSet,
88
reactOptions: LDReactOptions = defaultReactOptions,
99
targetFlags?: LDFlagSet,
10-
): { flags: LDFlagSet; _flags: LDFlagSet; flagKeyMap: LDFlagKeyMap } {
10+
): { flags: LDFlagSet; flagKeyMap: LDFlagKeyMap } {
1111
const filteredFlags = filterFlags(rawFlags, targetFlags);
1212
const flags: LDFlagSet = {};
1313
const flagKeyMap: LDFlagKeyMap = {};
@@ -27,7 +27,6 @@ export default function getFlagsProxy(
2727

2828
return {
2929
flags: reactOptions.sendEventsOnFlagRead ? toFlagsProxy(ldClient, flags, flagKeyMap) : flags,
30-
_flags: filteredFlags,
3130
flagKeyMap,
3231
};
3332
}
@@ -53,15 +52,14 @@ function hasFlag(flags: LDFlagSet, flagKey: string) {
5352
function toFlagsProxy(ldClient: LDClient, flags: LDFlagSet, flagKeyMap: LDFlagKeyMap): LDFlagSet {
5453
return new Proxy(flags, {
5554
// trap for reading a flag value that refreshes its value with `LDClient#variation` to trigger an evaluation event
56-
get(target, prop) {
57-
const flagKey = prop.toString();
58-
const currentValue = Reflect.get(target, flagKey);
55+
get(target, flagKey: string, receiver) {
56+
const currentValue = Reflect.get(target, flagKey, receiver);
5957
if (currentValue === undefined) {
6058
return;
6159
}
6260
const originalFlagKey = hasFlag(flagKeyMap, flagKey) ? flagKeyMap[flagKey] : flagKey;
6361
const nextValue = ldClient.variation(originalFlagKey, currentValue);
64-
Reflect.set(target, flagKey, nextValue);
62+
Reflect.set(target, flagKey, nextValue, receiver);
6563

6664
return nextValue;
6765
},

src/provider.test.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ describe('LDProvider', () => {
270270
expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, options, flags);
271271
expect(instance.setState).toHaveBeenCalledWith({
272272
flags: { devTestFlag: false, launchDoggly: false },
273-
_flags: { 'dev-test-flag': false, 'launch-doggly': false },
273+
unproxiedFlags: { 'dev-test-flag': false, 'launch-doggly': false },
274274
flagKeyMap: { devTestFlag: 'dev-test-flag', launchDoggly: 'launch-doggly' },
275275
ldClient: mockLDClient,
276276
});
@@ -289,7 +289,7 @@ describe('LDProvider', () => {
289289
await instance.componentDidMount();
290290
expect(instance.setState).toHaveBeenCalledWith({
291291
flags: { testFlag: true, anotherTestFlag: true },
292-
_flags: { 'test-flag': true, 'another-test-flag': true },
292+
unproxiedFlags: { 'test-flag': true, 'another-test-flag': true },
293293
flagKeyMap: { testFlag: 'test-flag', anotherTestFlag: 'another-test-flag' },
294294
ldClient: mockLDClient,
295295
});
@@ -323,13 +323,11 @@ describe('LDProvider', () => {
323323
const mockSetState = jest.spyOn(instance, 'setState');
324324

325325
await instance.componentDidMount();
326-
const callback = mockSetState.mock.calls[1][0] as (flags: LDFlagSet) => LDFlagSet;
327-
const newState = callback({ _flags: rawFlags });
328326

329327
expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function));
330-
expect(newState).toEqual({
328+
expect(mockSetState).toHaveBeenLastCalledWith({
331329
flags: { anotherTestFlag: true, testFlag: false },
332-
_flags: { 'another-test-flag': true, 'test-flag': false },
330+
unproxiedFlags: { 'another-test-flag': true, 'test-flag': false },
333331
flagKeyMap: { anotherTestFlag: 'another-test-flag', testFlag: 'test-flag' },
334332
});
335333
});
@@ -348,13 +346,11 @@ describe('LDProvider', () => {
348346
const mockSetState = jest.spyOn(instance, 'setState');
349347

350348
await instance.componentDidMount();
351-
const callback = mockSetState.mock.calls[1][0] as (flags: LDFlagSet) => LDFlagSet;
352-
const newState = callback({});
353349

354350
expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function));
355-
expect(newState).toEqual({
351+
expect(mockSetState).toHaveBeenLastCalledWith({
356352
flagKeyMap: {},
357-
_flags: { 'another-test-flag': false, 'test-flag': false },
353+
unproxiedFlags: { 'another-test-flag': false, 'test-flag': false },
358354
flags: { 'another-test-flag': false, 'test-flag': false },
359355
});
360356
});
@@ -391,7 +387,7 @@ describe('LDProvider', () => {
391387

392388
test('only updates to subscribed flags are pushed to the Provider', async () => {
393389
mockInitLDClient.mockImplementation(() => ({
394-
flags: { testFlag: 2 },
390+
flags: { 'test-flag': 2 },
395391
ldClient: mockLDClient,
396392
}));
397393
mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => {
@@ -410,12 +406,10 @@ describe('LDProvider', () => {
410406
const mockSetState = jest.spyOn(instance, 'setState');
411407

412408
await instance.componentDidMount();
413-
const callback = mockSetState.mock.calls[1][0] as (flags: LDFlagSet) => LDFlagSet;
414-
const newState = callback({});
415409

416-
expect(newState).toEqual({
410+
expect(mockSetState).toHaveBeenLastCalledWith({
417411
flags: { testFlag: 3 },
418-
_flags: { 'test-flag': 3 },
412+
unproxiedFlags: { 'test-flag': 3 },
419413
flagKeyMap: { testFlag: 'test-flag' },
420414
});
421415
});

src/provider.tsx

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import React, { Component, PropsWithChildren } from 'react';
2-
import { LDClient, LDFlagChangeset } from 'launchdarkly-js-client-sdk';
2+
import { LDClient, LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk';
33
import { EnhancedComponent, ProviderConfig, defaultReactOptions } from './types';
4-
import { Provider, LDContext as HocState } from './context';
4+
import { Provider, LDContext } from './context';
55
import initLDClient from './initLDClient';
66
import { camelCaseKeys, fetchFlags, getFlattenedFlagsFromChangeset } from './utils';
77
import getFlagsProxy from './getFlagsProxy';
88

9+
interface LDHocState extends LDContext {
10+
unproxiedFlags: LDFlagSet;
11+
}
12+
913
/**
1014
* The `LDProvider` is a component which accepts a config object which is used to
1115
* initialize `launchdarkly-js-client-sdk`.
@@ -23,8 +27,8 @@ import getFlagsProxy from './getFlagsProxy';
2327
* within your application. This provider is used inside the `withLDProviderHOC` and can be used instead to initialize
2428
* the `launchdarkly-js-client-sdk`. For async initialization, check out the `asyncWithLDProvider` function
2529
*/
26-
class LDProvider extends Component<PropsWithChildren<ProviderConfig>, HocState> implements EnhancedComponent {
27-
readonly state: Readonly<HocState>;
30+
class LDProvider extends Component<PropsWithChildren<ProviderConfig>, LDHocState> implements EnhancedComponent {
31+
readonly state: Readonly<LDHocState>;
2832

2933
constructor(props: ProviderConfig) {
3034
super(props);
@@ -33,7 +37,7 @@ class LDProvider extends Component<PropsWithChildren<ProviderConfig>, HocState>
3337

3438
this.state = {
3539
flags: {},
36-
_flags: {},
40+
unproxiedFlags: {},
3741
flagKeyMap: {},
3842
ldClient: undefined,
3943
};
@@ -42,10 +46,9 @@ class LDProvider extends Component<PropsWithChildren<ProviderConfig>, HocState>
4246
const { bootstrap } = options;
4347
if (bootstrap && bootstrap !== 'localStorage') {
4448
const { useCamelCaseFlagKeys } = this.getReactOptions();
45-
const flags = useCamelCaseFlagKeys ? camelCaseKeys(bootstrap) : bootstrap;
4649
this.state = {
47-
flags,
48-
_flags: bootstrap,
50+
flags: useCamelCaseFlagKeys ? camelCaseKeys(bootstrap) : bootstrap,
51+
unproxiedFlags: bootstrap,
4952
flagKeyMap: {},
5053
ldClient: undefined,
5154
};
@@ -60,8 +63,12 @@ class LDProvider extends Component<PropsWithChildren<ProviderConfig>, HocState>
6063
ldClient.on('change', (changes: LDFlagChangeset) => {
6164
const reactOptions = this.getReactOptions();
6265
const updates = getFlattenedFlagsFromChangeset(changes, targetFlags);
66+
const unproxiedFlags = {
67+
...this.state.unproxiedFlags,
68+
...updates,
69+
};
6370
if (Object.keys(updates).length > 0) {
64-
this.setState(({ _flags }) => getFlagsProxy(ldClient, { ..._flags, ...updates }, reactOptions, targetFlags));
71+
this.setState({ unproxiedFlags, ...getFlagsProxy(ldClient, unproxiedFlags, reactOptions, targetFlags) });
6572
}
6673
});
6774
};
@@ -70,15 +77,15 @@ class LDProvider extends Component<PropsWithChildren<ProviderConfig>, HocState>
7077
const { clientSideID, flags, options, user } = this.props;
7178
let ldClient = await this.props.ldClient;
7279
const reactOptions = this.getReactOptions();
73-
let fetchedFlags;
80+
let unproxiedFlags;
7481
if (ldClient) {
75-
fetchedFlags = fetchFlags(ldClient, flags);
82+
unproxiedFlags = fetchFlags(ldClient, flags);
7683
} else {
7784
const initialisedOutput = await initLDClient(clientSideID, user, options, flags);
78-
fetchedFlags = initialisedOutput.flags;
85+
unproxiedFlags = initialisedOutput.flags;
7986
ldClient = initialisedOutput.ldClient;
8087
}
81-
this.setState({ ...getFlagsProxy(ldClient, fetchedFlags, reactOptions, flags), ldClient });
88+
this.setState({ unproxiedFlags, ...getFlagsProxy(ldClient, unproxiedFlags, reactOptions, flags), ldClient });
8289
this.subscribeToChanges(ldClient);
8390
};
8491

@@ -100,7 +107,9 @@ class LDProvider extends Component<PropsWithChildren<ProviderConfig>, HocState>
100107
}
101108

102109
render() {
103-
return <Provider value={this.state}>{this.props.children}</Provider>;
110+
const { flags, flagKeyMap, ldClient } = this.state;
111+
112+
return <Provider value={{ flags, flagKeyMap, ldClient }}>{this.props.children}</Provider>;
104113
}
105114
}
106115

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export interface AllFlagsLDClient {
132132
}
133133

134134
/**
135-
* Mag of camelized flag key to original unmodified flag key.
135+
* Map of camelized flag key to original unmodified flag key.
136136
*/
137137
export interface LDFlagKeyMap {
138138
[camelCasedKey: string]: string;

src/utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { LDClient, LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk';
22
import camelCase from 'lodash.camelcase';
33

4-
// Note that this is no longer used by the SDK, but is still exported for backwards comatability
54
/**
65
* Transforms a set of flags so that their keys are camelCased. This function ignores
76
* flag keys which start with `$`.

0 commit comments

Comments
 (0)