From 9144ded054a6750c6f0ed7ad34587fcae7044baf Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 Nov 2025 13:37:44 -0300 Subject: [PATCH] refactor: move treatment evaluation logic from utils to hook modules for code tree shaking --- src/__tests__/utils.test.ts | 65 +++++++++++++++----- src/__tests__/withSplitTreatments.test.tsx | 6 +- src/useTreatment.ts | 13 +++- src/useTreatmentWithConfig.ts | 13 +++- src/useTreatments.ts | 17 +++++- src/useTreatmentsWithConfig.ts | 17 +++++- src/utils.ts | 69 ++++------------------ 7 files changed, 119 insertions(+), 81 deletions(-) diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 00c5b39..355c88c 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -1,26 +1,61 @@ import { CONTROL, CONTROL_WITH_CONFIG } from '../constants'; -import { getControlTreatments } from '../utils'; +import { getTreatments, getTreatment } from '../utils'; +import { sdkBrowserWithConfig } from './testUtils/sdkConfigs'; -describe('getControlTreatments', () => { +const factoryWithoutFallbacks = { + settings: {} +} as SplitIO.IBrowserSDK; + +const factoryWithFallbacks = { + settings: sdkBrowserWithConfig +} as SplitIO.IBrowserSDK + +describe('getTreatments', () => { it('should return an empty object if an empty array is provided', () => { - expect(Object.values(getControlTreatments([], true)).length).toBe(0); - expect(Object.values(getControlTreatments([], false)).length).toBe(0); + expect(getTreatments([], true)).toEqual({}); + expect(getTreatments([], false)).toEqual({}); }); it('should return an object with control treatments if an array of feature flag names is provided', () => { const featureFlagNames = ['split1', 'split2']; - const treatmentsWithConfig: SplitIO.TreatmentsWithConfig = getControlTreatments(featureFlagNames, true); - featureFlagNames.forEach((featureFlagName) => { - expect(treatmentsWithConfig[featureFlagName]).toBe(CONTROL_WITH_CONFIG); - }); - expect(Object.keys(treatmentsWithConfig).length).toBe(featureFlagNames.length); - - const treatments: SplitIO.Treatments = getControlTreatments(featureFlagNames, false); - featureFlagNames.forEach((featureFlagName) => { - expect(treatments[featureFlagName]).toBe(CONTROL); - }); - expect(Object.keys(treatments).length).toBe(featureFlagNames.length); + const treatmentsWithConfig: SplitIO.TreatmentsWithConfig = getTreatments(featureFlagNames, true); + expect(treatmentsWithConfig).toEqual({ 'split1': CONTROL_WITH_CONFIG, 'split2': CONTROL_WITH_CONFIG }); + + const treatments: SplitIO.Treatments = getTreatments(featureFlagNames, false); + expect(treatments).toEqual({ 'split1': CONTROL, 'split2': CONTROL }); + + expect(getTreatments(featureFlagNames, true, factoryWithoutFallbacks)).toEqual({ 'split1': CONTROL_WITH_CONFIG, 'split2': CONTROL_WITH_CONFIG }); + expect(getTreatments(featureFlagNames, false, factoryWithoutFallbacks)).toEqual({ 'split1': CONTROL, 'split2': CONTROL }); + }); + + it('should return an object with fallback or control treatments if an array of feature flag names and factory are provided', () => { + const featureFlagNames = ['split1', 'ff1']; + const treatmentsWithConfig: SplitIO.TreatmentsWithConfig = getTreatments(featureFlagNames, true, factoryWithFallbacks); + expect(treatmentsWithConfig).toEqual({ 'split1': { treatment: 'control_global', config: null }, 'ff1': { treatment: 'control_ff1', config: 'control_ff1_config' } }); + + const treatments: SplitIO.Treatments = getTreatments(featureFlagNames, false, factoryWithFallbacks); + expect(treatments).toEqual({ 'split1': 'control_global', 'ff1': 'control_ff1' }); + }); + +}); + +describe('getTreatment', () => { + + it('should return control treatments', () => { + expect(getTreatment('any', true)).toEqual(CONTROL_WITH_CONFIG); + expect(getTreatment('any', false)).toEqual(CONTROL); + + expect(getTreatment('any', true, factoryWithoutFallbacks)).toEqual(CONTROL_WITH_CONFIG); + expect(getTreatment('any', false, factoryWithoutFallbacks)).toEqual(CONTROL); + }); + + it('should return fallback treatments if a factory with fallback treatments is provided', () => { + const treatmentWithConfig: SplitIO.TreatmentWithConfig = getTreatment('split1', true, factoryWithFallbacks); + expect(treatmentWithConfig).toEqual({ treatment: 'control_global', config: null }); + + const treatment: SplitIO.Treatment = getTreatment('ff1', false, factoryWithFallbacks); + expect(treatment).toEqual('control_ff1' ); }); }); diff --git a/src/__tests__/withSplitTreatments.test.tsx b/src/__tests__/withSplitTreatments.test.tsx index dfaf3c2..88e1dd9 100644 --- a/src/__tests__/withSplitTreatments.test.tsx +++ b/src/__tests__/withSplitTreatments.test.tsx @@ -14,14 +14,14 @@ import { INITIAL_STATUS } from './testUtils/utils'; import { withSplitFactory } from '../withSplitFactory'; import { withSplitClient } from '../withSplitClient'; import { withSplitTreatments } from '../withSplitTreatments'; -import { getControlTreatments } from '../utils'; +import { getTreatments } from '../utils'; const featureFlagNames = ['split1', 'split2']; describe('withSplitTreatments', () => { it(`passes Split props and outer props to the child. - In this test, the value of "props.treatments" is obtained by the function "getControlTreatments", + In this test, the value of "props.treatments" is obtained by the function "getTreatments", and not "client.getTreatmentsWithConfig" since the client is not ready.`, () => { const Component = withSplitFactory(sdkBrowser)<{ outerProp1: string, outerProp2: number }>( @@ -36,7 +36,7 @@ describe('withSplitTreatments', () => { ...INITIAL_STATUS, factory: factory, client: clientMock, outerProp1: 'outerProp1', outerProp2: 2, - treatments: getControlTreatments(featureFlagNames, true), + treatments: getTreatments(featureFlagNames, true), }); return null; diff --git a/src/useTreatment.ts b/src/useTreatment.ts index 1e743e0..19daa8b 100644 --- a/src/useTreatment.ts +++ b/src/useTreatment.ts @@ -1,8 +1,19 @@ import * as React from 'react'; -import { memoizeGetTreatment } from './utils'; +import memoizeOne from 'memoize-one'; +import { argsAreEqual, getTreatment } from './utils'; import { IUseTreatmentResult, IUseTreatmentOptions } from './types'; import { useSplitClient } from './useSplitClient'; +function evaluateFeatureFlag(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) { + return client && client.getStatus().isOperational ? + client.getTreatment(names[0], attributes, options) : + getTreatment(names[0], false, factory); +} + +function memoizeGetTreatment() { + return memoizeOne(evaluateFeatureFlag, argsAreEqual); +} + /** * `useTreatment` is a hook that returns an Split Context object extended with a `treatment` property. * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatment()` method. diff --git a/src/useTreatmentWithConfig.ts b/src/useTreatmentWithConfig.ts index b7790d3..0fecfac 100644 --- a/src/useTreatmentWithConfig.ts +++ b/src/useTreatmentWithConfig.ts @@ -1,8 +1,19 @@ import * as React from 'react'; -import { memoizeGetTreatmentWithConfig } from './utils'; +import memoizeOne from 'memoize-one'; +import { argsAreEqual, getTreatment } from './utils'; import { IUseTreatmentWithConfigResult, IUseTreatmentOptions } from './types'; import { useSplitClient } from './useSplitClient'; +function evaluateFeatureFlagWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) { + return client && client.getStatus().isOperational ? + client.getTreatmentWithConfig(names[0], attributes, options) : + getTreatment(names[0], true, factory); +} + +function memoizeGetTreatmentWithConfig() { + return memoizeOne(evaluateFeatureFlagWithConfig, argsAreEqual); +} + /** * `useTreatmentWithConfig` is a hook that returns an Split Context object extended with a `treatment` property. * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatmentWithConfig()` method. diff --git a/src/useTreatments.ts b/src/useTreatments.ts index 45c29a9..1cb22b3 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -1,8 +1,23 @@ import * as React from 'react'; -import { memoizeGetTreatments } from './utils'; +import memoizeOne from 'memoize-one'; +import { argsAreEqual, getTreatments } from './utils'; import { IUseTreatmentsResult, IUseTreatmentsOptions } from './types'; import { useSplitClient } from './useSplitClient'; +function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) { + return client && client.getStatus().isOperational && (names || flagSets) ? + names ? + client.getTreatments(names, attributes, options) : + client.getTreatmentsByFlagSets(flagSets!, attributes, options) : + names ? + getTreatments(names, false, factory) : + {} // empty object when evaluating with flag sets and client is not ready +} + +export function memoizeGetTreatments() { + return memoizeOne(evaluateFeatureFlags, argsAreEqual); +} + /** * `useTreatments` is a hook that returns an Split Context object extended with a `treatments` property object that contains feature flag evaluations. * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatments()` method if the `names` option is provided, diff --git a/src/useTreatmentsWithConfig.ts b/src/useTreatmentsWithConfig.ts index 0efb094..0cbd7c3 100644 --- a/src/useTreatmentsWithConfig.ts +++ b/src/useTreatmentsWithConfig.ts @@ -1,8 +1,23 @@ import * as React from 'react'; -import { memoizeGetTreatmentsWithConfig } from './utils'; +import memoizeOne from 'memoize-one'; +import { argsAreEqual, getTreatments } from './utils'; import { IUseTreatmentsOptions, IUseTreatmentsWithConfigResult } from './types'; import { useSplitClient } from './useSplitClient'; +function evaluateFeatureFlagsWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) { + return client && client.getStatus().isOperational && (names || flagSets) ? + names ? + client.getTreatmentsWithConfig(names, attributes, options) : + client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes, options) : + names ? + getTreatments(names, true, factory) : + {} // empty object when evaluating with flag sets and client is not ready +} + +function memoizeGetTreatmentsWithConfig() { + return memoizeOne(evaluateFeatureFlagsWithConfig, argsAreEqual); +} + /** * `useTreatmentsWithConfig` is a hook that returns an Split Context object extended with a `treatments` property object that contains feature flag evaluations. * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatmentsWithConfig()` method if the `names` option is provided, diff --git a/src/utils.ts b/src/utils.ts index 8ccdfa2..dddb3f7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,3 @@ -import memoizeOne from 'memoize-one'; import shallowEqual from 'shallowequal'; import { CONTROL, CONTROL_WITH_CONFIG } from './constants'; import { ISplitStatus } from './types'; @@ -40,12 +39,12 @@ export function initAttributes(client?: SplitIO.IBrowserClient, attributes?: Spl if (client && attributes) client.setAttributes(attributes); } -// Utils used to retrieve treatments when the client is not operational: +// Utils used to retrieve fallback or control treatments when the client is not operational: -function resolveFallback(flagName: string, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentWithConfig; -function resolveFallback(flagName: string, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment; -function resolveFallback(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment | SplitIO.TreatmentWithConfig; -function resolveFallback(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK) { +export function getTreatment(flagName: string, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentWithConfig; +export function getTreatment(flagName: string, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment; +export function getTreatment(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK): SplitIO.Treatment | SplitIO.TreatmentWithConfig; +export function getTreatment(flagName: string, withConfig: boolean, factory?: SplitIO.IBrowserSDK) { if (factory && factory.settings.fallbackTreatments) { const fallbacks = factory.settings.fallbackTreatments; @@ -61,9 +60,9 @@ function resolveFallback(flagName: string, withConfig: boolean, factory?: SplitI return withConfig ? CONTROL_WITH_CONFIG : CONTROL; } -export function getControlTreatments(featureFlagNames: unknown, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentsWithConfig; -export function getControlTreatments(featureFlagNames: unknown, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatments; -export function getControlTreatments(featureFlagNames: unknown, withConfig: boolean, factory?: SplitIO.IBrowserSDK) { +export function getTreatments(featureFlagNames: unknown, withConfig: true, factory?: SplitIO.IBrowserSDK): SplitIO.TreatmentsWithConfig; +export function getTreatments(featureFlagNames: unknown, withConfig: false, factory?: SplitIO.IBrowserSDK): SplitIO.Treatments; +export function getTreatments(featureFlagNames: unknown, withConfig: boolean, factory?: SplitIO.IBrowserSDK) { // validate feature flag names if (!Array.isArray(featureFlagNames)) return {}; @@ -74,7 +73,7 @@ export function getControlTreatments(featureFlagNames: unknown, withConfig: bool // return control or fallback treatment for each validated feature flag name return (featureFlagNames as string[]).reduce((pValue: SplitIO.Treatments | SplitIO.TreatmentsWithConfig, featureFlagName: string) => { - pValue[featureFlagName] = resolveFallback(featureFlagName, withConfig, factory); + pValue[featureFlagName] = getTreatment(featureFlagName, withConfig, factory); return pValue; }, {}); } @@ -84,7 +83,7 @@ export function getControlTreatments(featureFlagNames: unknown, withConfig: bool * The result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag names and attributes. */ -function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { +export function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { return newArgs[0] === lastArgs[0] && // client newArgs[1] === lastArgs[1] && // lastUpdate shallowEqual(newArgs[2], lastArgs[2]) && // names @@ -92,51 +91,3 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { shallowEqual(newArgs[4], lastArgs[4]) && // client attributes shallowEqual(newArgs[5], lastArgs[5]); // flagSets } - -function evaluateFeatureFlagsWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) { - return client && client.getStatus().isOperational && (names || flagSets) ? - names ? - client.getTreatmentsWithConfig(names, attributes, options) : - client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes, options) : - names ? - getControlTreatments(names, true, factory) : - {} // empty object when evaluating with flag sets and client is not ready -} - -export function memoizeGetTreatmentsWithConfig() { - return memoizeOne(evaluateFeatureFlagsWithConfig, argsAreEqual); -} - -function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[], options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) { - return client && client.getStatus().isOperational && (names || flagSets) ? - names ? - client.getTreatments(names, attributes, options) : - client.getTreatmentsByFlagSets(flagSets!, attributes, options) : - names ? - getControlTreatments(names, false, factory) : - {} // empty object when evaluating with flag sets and client is not ready -} - -export function memoizeGetTreatments() { - return memoizeOne(evaluateFeatureFlags, argsAreEqual); -} - -function evaluateFeatureFlagWithConfig(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) { - return client && client.getStatus().isOperational ? - client.getTreatmentWithConfig(names[0], attributes, options) : - resolveFallback(names[0], true, factory); -} - -export function memoizeGetTreatmentWithConfig() { - return memoizeOne(evaluateFeatureFlagWithConfig, argsAreEqual); -} - -function evaluateFeatureFlag(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names: string[], attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, _flagSets?: undefined, options?: SplitIO.EvaluationOptions, factory?: SplitIO.IBrowserSDK) { - return client && client.getStatus().isOperational ? - client.getTreatment(names[0], attributes, options) : - resolveFallback(names[0], false, factory); -} - -export function memoizeGetTreatment() { - return memoizeOne(evaluateFeatureFlag, argsAreEqual); -}