diff --git a/src/asyncWithLDProvider.test.tsx b/src/asyncWithLDProvider.test.tsx index 2d08746..f9796d2 100644 --- a/src/asyncWithLDProvider.test.tsx +++ b/src/asyncWithLDProvider.test.tsx @@ -179,4 +179,19 @@ describe('asyncWithLDProvider', () => { expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, defaultReactOptions, options, flags); expect(receivedNode).toHaveTextContent('{"devTestFlag":true,"launchDoggly":true}'); }); + + test('only updates to subscribed flags are pushed to the Provider', async () => { + mockInitLDClient.mockImplementation(() => ({ + flags: { testFlag: 2 }, + ldClient: mockLDClient, + })); + mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => { + cb({ 'test-flag': { current: 3, previous: 2 }, 'another-test-flag': { current: false, previous: true } }); + }); + const options: LDOptions = {}; + const subscribedFlags = { 'test-flag': 1 }; + const receivedNode = await renderWithConfig({ clientSideID, user, options, flags: subscribedFlags }); + + expect(receivedNode).toHaveTextContent('{"testFlag":3}'); + }); }); diff --git a/src/asyncWithLDProvider.tsx b/src/asyncWithLDProvider.tsx index 8b35a61..688d56e 100644 --- a/src/asyncWithLDProvider.tsx +++ b/src/asyncWithLDProvider.tsx @@ -1,10 +1,9 @@ import React, { useState, useEffect, FunctionComponent } from 'react'; -import camelCase from 'lodash.camelcase'; import { LDFlagSet, LDFlagChangeset } from 'launchdarkly-js-client-sdk'; import { defaultReactOptions, ProviderConfig } from './types'; import { Provider } from './context'; import initLDClient from './initLDClient'; -import { camelCaseKeys } from './utils'; +import { camelCaseKeys, getFlattenedFlagsFromChangeset } from './utils'; /** * This is an async function which initializes LaunchDarkly's JS SDK (`launchdarkly-js-client-sdk`) @@ -46,14 +45,10 @@ export default async function asyncWithLDProvider(config: ProviderConfig) { } ldClient.on('change', (changes: LDFlagChangeset) => { - const flattened: LDFlagSet = {}; - for (const key in changes) { - // tslint:disable-next-line:no-unsafe-any - const flagKey = reactOptions.useCamelCaseFlagKeys ? camelCase(key) : key; - flattened[flagKey] = changes[key].current; + const flattened: LDFlagSet = getFlattenedFlagsFromChangeset(changes, flags, reactOptions); + if (Object.keys(flattened).length > 0) { + setLDData(prev => ({ ...prev, flags: { ...prev.flags, ...flattened } })); } - - setLDData(prev => ({ ...prev, flags: { ...prev.flags, ...flattened } })); }); }, []); diff --git a/src/provider.test.tsx b/src/provider.test.tsx index 7de1762..4bc7e49 100644 --- a/src/provider.test.tsx +++ b/src/provider.test.tsx @@ -311,4 +311,31 @@ describe('LDProvider', () => { expect(mockInitLDClient).toHaveBeenCalledWith(clientSideID, user, defaultReactOptions, options, undefined); }); + + test('only updates to subscribed flags are pushed to the Provider', async () => { + mockInitLDClient.mockImplementation(() => ({ + flags: { testFlag: 2 }, + ldClient: mockLDClient, + })); + mockLDClient.on.mockImplementation((e: string, cb: (c: LDFlagChangeset) => void) => { + cb({ 'test-flag': { current: 3, previous: 2 }, 'another-test-flag': { current: false, previous: true } }); + }); + const options: LDOptions = {}; + const user: LDUser = { key: 'yus', name: 'yus ng' }; + const subscribedFlags = { 'test-flag': 1 }; + const props: ProviderConfig = { clientSideID, user, options, flags: subscribedFlags }; + const LaunchDarklyApp = ( + + + + ); + const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent; + const mockSetState = jest.spyOn(instance, 'setState'); + + await instance.componentDidMount(); + const callback = mockSetState.mock.calls[1][0] as (flags: LDFlagSet) => LDFlagSet; + const newState = callback({}); + + expect(newState).toEqual({ flags: { testFlag: 3 } }); + }); }); diff --git a/src/provider.tsx b/src/provider.tsx index b4e8717..cceddb9 100644 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; -import camelCase from 'lodash.camelcase'; import { LDClient, LDFlagSet, LDFlagChangeset } from 'launchdarkly-js-client-sdk'; import { EnhancedComponent, ProviderConfig, defaultReactOptions } from './types'; import { Provider, LDContext as HocState } from './context'; import initLDClient from './initLDClient'; -import { camelCaseKeys } from './utils'; +import { camelCaseKeys, getFlattenedFlagsFromChangeset } from './utils'; /** * The `LDProvider` is a component which accepts a config object which is used to @@ -52,15 +51,12 @@ class LDProvider extends React.Component implements En getReactOptions = () => ({ ...defaultReactOptions, ...this.props.reactOptions }); subscribeToChanges = (ldClient: LDClient) => { + const { flags: targetFlags } = this.props; ldClient.on('change', (changes: LDFlagChangeset) => { - const flattened: LDFlagSet = {}; - for (const key in changes) { - // tslint:disable-next-line:no-unsafe-any - const { useCamelCaseFlagKeys } = this.getReactOptions(); - const flagKey = useCamelCaseFlagKeys ? camelCase(key) : key; - flattened[flagKey] = changes[key].current; + const flattened: LDFlagSet = getFlattenedFlagsFromChangeset(changes, targetFlags, this.getReactOptions()); + if (Object.keys(flattened).length > 0) { + this.setState(({ flags }) => ({ flags: { ...flags, ...flattened } })); } - this.setState(({ flags }) => ({ flags: { ...flags, ...flattened } })); }); }; diff --git a/src/utils.test.ts b/src/utils.test.ts index db45c4c..02bc348 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,4 +1,6 @@ -import { camelCaseKeys } from './utils'; +import { camelCaseKeys, getFlattenedFlagsFromChangeset } from './utils'; +import { LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk'; +import { LDReactOptions } from './types'; describe('Utils', () => { test('camelCaseKeys should ignore system keys', () => { @@ -15,4 +17,59 @@ describe('Utils', () => { const result = camelCaseKeys(bootstrap); expect(result).toEqual({ testFlag: true, anotherTestFlag: false }); }); + + test('getFlattenedFlagsFromChangeset should return current values of all flags when no targetFlags specified', () => { + const targetFlags: LDFlagSet | undefined = undefined; + const flagChanges: LDFlagChangeset = { + 'test-flag': { current: true, previous: false }, + 'another-test-flag': { current: false, previous: true }, + }; + const reactOptions: LDReactOptions = { + useCamelCaseFlagKeys: true, + }; + const flattened = getFlattenedFlagsFromChangeset(flagChanges, targetFlags, reactOptions); + + expect(flattened).toEqual({ anotherTestFlag: false, testFlag: true }); + }); + + test('getFlattenedFlagsFromChangeset should return current values only of targetFlags when specified', () => { + const targetFlags: LDFlagSet | undefined = { 'test-flag': false }; + const flagChanges: LDFlagChangeset = { + 'test-flag': { current: true, previous: false }, + 'another-test-flag': { current: false, previous: true }, + }; + const reactOptions: LDReactOptions = { + useCamelCaseFlagKeys: true, + }; + const flattened = getFlattenedFlagsFromChangeset(flagChanges, targetFlags, reactOptions); + + expect(flattened).toEqual({ testFlag: true }); + }); + + test('getFlattenedFlagsFromChangeset should return empty LDFlagSet when no targetFlags are changed ', () => { + const targetFlags: LDFlagSet | undefined = { 'test-flag': false }; + const flagChanges: LDFlagChangeset = { + 'another-test-flag': { current: false, previous: true }, + }; + const reactOptions: LDReactOptions = { + useCamelCaseFlagKeys: true, + }; + const flattened = getFlattenedFlagsFromChangeset(flagChanges, targetFlags, reactOptions); + + expect(Object.keys(flattened)).toHaveLength(0); + }); + + test('getFlattenedFlagsFromChangeset should not change flags to camelCase when reactOptions.useCamelCaseFlagKeys is false ', () => { + const targetFlags: LDFlagSet | undefined = undefined; + const flagChanges: LDFlagChangeset = { + 'test-flag': { current: true, previous: false }, + 'another-test-flag': { current: false, previous: true }, + }; + const reactOptions: LDReactOptions = { + useCamelCaseFlagKeys: false, + }; + const flattened = getFlattenedFlagsFromChangeset(flagChanges, targetFlags, reactOptions); + + expect(flattened).toEqual({ 'another-test-flag': false, 'test-flag': true }); + }); }); diff --git a/src/utils.ts b/src/utils.ts index 88e2eec..66da194 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ -import { LDFlagSet } from 'launchdarkly-js-client-sdk'; +import { LDFlagChangeset, LDFlagSet } from 'launchdarkly-js-client-sdk'; import camelCase from 'lodash.camelcase'; +import { LDReactOptions } from './types'; /** * Transforms a set of flags so that their keys are camelCased. This function ignores @@ -20,4 +21,31 @@ export const camelCaseKeys = (rawFlags: LDFlagSet) => { return flags; }; -export default { camelCaseKeys }; +/** + * Gets the flags to pass to the provider from the changeset. + * + * @param changes the `LDFlagChangeset` from the ldClient onchange handler. + * @param targetFlags if targetFlags are specified, changes to other flags are ignored and not returned in the + * flattened `LDFlagSet` + * @param reactOptions reactOptions.useCamelCaseFlagKeys determines whether to change the flag keys to camelCase + * @return an `LDFlagSet` with the current flag values from the LDFlagChangeset filtered by `targetFlags`. The returned + * object may be empty `{}` if none of the targetFlags were changed. + */ +export const getFlattenedFlagsFromChangeset = ( + changes: LDFlagChangeset, + targetFlags: LDFlagSet | undefined, + reactOptions: LDReactOptions, +): LDFlagSet => { + const flattened: LDFlagSet = {}; + for (const key in changes) { + if (targetFlags === undefined || targetFlags[key] !== undefined) { + // tslint:disable-next-line:no-unsafe-any + const flagKey = reactOptions.useCamelCaseFlagKeys ? camelCase(key) : key; + flattened[flagKey] = changes[key].current; + } + } + + return flattened; +}; + +export default { camelCaseKeys, getFlattenedFlagsFromChangeset };