diff --git a/CHANGES.txt b/CHANGES.txt index 926feea..0346c5e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +1.13.0 (May 24, 2024) + - Added a new `getStatus` helper function to retrieve the status properties of the SDK manager and clients: `isReady`, `isReadyFromCache`, `hasTimedout`, and `isDestroyed`. + - Added new `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors as alternatives to the `selectTreatmentValue` and `selectTreatmentWithConfig` selectors, respectively. + The new selectors retrieve an object with the treatment and the status properties of the client associated with the provided key, allowing conditional rendering based on the SDK status. + 1.12.0 (May 10, 2024) - Updated @splitsoftware/splitio package to version 10.26.0 that includes minor updates: - Added support for targeting rules based on semantic versions (https://semver.org/). @@ -55,7 +60,7 @@ 1.6.0 (Jul 7, 2022) - Updated @splitsoftware/splitio dependency to version 10.20.0, which includes: - - Added a new config option to control the tasks that listen or poll for updates on feature flags and segments, via the new config sync.enabled . Running online Split will always pull the most recent updates upon initialization, this only affects updates fetching on a running instance. Useful when a consistent session experience is a must or to save resources when updates are not being used. + - Added a new config option to control the tasks that listen or poll for updates on feature flags and segments, via the new config `sync.enabled`. Running online Split will always pull the most recent updates upon initialization, this only affects updates fetching on a running instance. Useful when a consistent session experience is a must or to save resources when updates are not being used. - Updated telemetry logic to track the anonymous config for user consent flag set to declined or unknown. - Updated submitters logic, to avoid duplicating the post of impressions to Split cloud when the SDK is destroyed while its periodic post of impressions is running. - Added `scheduler.telemetryRefreshRate` property to SDK configuration, and deprecated `scheduler.metricsRefreshRate` property. @@ -76,7 +81,7 @@ - Updated localhost mode to emit SDK_READY_FROM_CACHE event in Browser when using localStorage (issue https://github.com/splitio/react-client/issues/34). - Updated streaming logic to use the newest version of our streaming service, including: - Integration with Auth service V2, connecting to the new channels and applying the received connection delay. - - Implemented handling of the new MySegmentsV2 notification types (SegmentRemoval, KeyList, Bounded and Unbounded) + - Implemented handling of the new MySegmentsV2 notification types (SegmentRemoval, KeyList, Bounded and Unbounded). - New control notification for environment scoped streaming reset. - Updated Enzyme and Jest development dependencies to fix vulnerabilities. @@ -104,13 +109,13 @@ - Added an optional callback parameter to `destroySplitSdk` action creator: `onDestroy`, to listen when the SDK has gracefully shut down. 1.1.0 (May 11, 2020) - - Bugfixing - incorrect evaluation of feature flags on browser when using `getTreatments` with a different user key than the default, caused by not waiting the fetch of segments. + - Bugfixing - Incorrect evaluation of feature flags on browser when using `getTreatments` with a different user key than the default, caused by not waiting the fetch of segments (Related to issue https://github.com/splitio/redux-client/issues/9). - Added `destroySplitSdk` action creator to gracefully shutdown the SDK. - Added two new status properties to split's piece of state: `hasTimedout` and `isDestroyed` to better reflect the current state of the associated factory. 1.0.1 (April 6, 2020) - - Updated dependencies to fix vulnerabilities - - Bugfixing - support numbers as user keys + - Updated dependencies to fix vulnerabilities. + - Bugfixing - Support numbers as user keys. 1.0.0 (January 24, 2020) - Initial public release! diff --git a/README.md b/README.md index c0b56a7..9041027 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ import React from 'react'; import { createStore, applyMiddleware, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import { splitReducer, initSplitSdk, getTreatments, - selectTreatmentValue, connectSplit } from '@splitsoftware/splitio-redux' + selectTreatmentAndStatus, connectSplit } from '@splitsoftware/splitio-redux' // Init Redux store const store = createStore( @@ -47,12 +47,12 @@ store.dispatch(getTreatments({ splitNames: 'FEATURE_FLAG_NAME' })) // Connect your component to splitio's piece of state const MyComponent = connectSplit()(({ splitio }) => { - // Check SDK readiness using isReady property - if (!splitio.isReady) - return
Loading SDK ...
; - // Select a treatment value - const treatment = selectTreatmentValue(splitio, 'FEATURE_FLAG_NAME') + const { treatment, isReady } = selectTreatmentAndStatus(splitio, 'FEATURE_FLAG_NAME') + + // Check SDK client readiness using isReady property + if (!isReady) return
Loading SDK ...
; + if (treatment === 'on') { // return JSX for 'on' treatment } else if (treatment === 'off') { diff --git a/package-lock.json b/package-lock.json index ffcd7b5..76a4c02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.0", + "version": "1.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-redux", - "version": "1.12.0", + "version": "1.13.0", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio": "10.26.0", diff --git a/package.json b/package.json index 83030b7..9da3da2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-redux", - "version": "1.12.0", + "version": "1.13.0", "description": "A library to easily use Split JS SDK with Redux and React Redux", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/__tests__/helpers.browser.test.ts b/src/__tests__/helpers.browser.test.ts index 64e4f30..feabf04 100644 --- a/src/__tests__/helpers.browser.test.ts +++ b/src/__tests__/helpers.browser.test.ts @@ -1,12 +1,14 @@ /** Mocks */ -import { mockSdk } from './utils/mockBrowserSplitSdk'; +import { mockSdk, Event } from './utils/mockBrowserSplitSdk'; jest.mock('@splitsoftware/splitio', () => { return { SplitFactory: mockSdk() }; }); /** Constants, types, utils */ import { sdkBrowserConfig } from './utils/sdkConfigs'; +import { STATUS_INITIAL } from './utils/storeState'; import { + getTreatments, initSplitSdk, splitSdk, } from '../asyncActions'; @@ -18,6 +20,7 @@ import { getSplit, getSplits, track, + getStatus, } from '../helpers'; const featureFlagNames: string[] = ['split_1', 'split_2']; @@ -171,3 +174,35 @@ describe('track', () => { }); }); + +describe('getStatus', () => { + + beforeEach(() => { + splitSdk.factory = null; + }); + + it('should return the default status if the SDK was not initialized', () => { + expect(getStatus()).toEqual(STATUS_INITIAL); + }); + + it('should return the status of the client associated to the provided key', () => { + initSplitSdk({ config: sdkBrowserConfig }); + getTreatments({ key: 'user_2', splitNames: ['split_1'] }); + (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); + (splitSdk.factory as any).client('user_2').__emitter__.emit(Event.SDK_READY_FROM_CACHE); + + // Main client + const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true }; + expect(getStatus()).toEqual(MAIN_CLIENT_STATUS); + expect(getStatus(sdkBrowserConfig.core.key)).toEqual(MAIN_CLIENT_STATUS); + expect(getStatus({ matchingKey: sdkBrowserConfig.core.key as string, bucketingKey: '' })).toEqual(MAIN_CLIENT_STATUS); + + // Client for user_2 + const USER_2_STATUS = { ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true }; + expect(getStatus('user_2')).toEqual(USER_2_STATUS); + expect(getStatus({ matchingKey: 'user_2', bucketingKey: '' })).toEqual(USER_2_STATUS); + + // Non-existing client + expect(getStatus('non_existing_key')).toEqual(STATUS_INITIAL); + }); +}); diff --git a/src/__tests__/helpers.node.test.ts b/src/__tests__/helpers.node.test.ts index cd1db86..9f90fcf 100644 --- a/src/__tests__/helpers.node.test.ts +++ b/src/__tests__/helpers.node.test.ts @@ -1,11 +1,12 @@ /** Mocks */ -import { mockSdk } from './utils/mockNodeSplitSdk'; +import { mockSdk, Event } from './utils/mockNodeSplitSdk'; jest.mock('@splitsoftware/splitio', () => { return { SplitFactory: mockSdk() }; }); /** Constants, types, utils */ import { sdkNodeConfig } from './utils/sdkConfigs'; +import { STATUS_INITIAL } from './utils/storeState'; import { initSplitSdk, splitSdk, @@ -18,6 +19,7 @@ import { getSplit, getSplits, track, + getStatus, } from '../helpers'; const featureFlagNames: string[] = ['split_1', 'split_2']; @@ -143,3 +145,23 @@ describe('track', () => { }); }); + +describe('getStatus', () => { + + beforeEach(() => { + splitSdk.factory = null; + }); + + it('should return the default status if the SDK was not initialized', () => { + expect(getStatus()).toEqual(STATUS_INITIAL); + }); + + it('should return the status of the main client', () => { + initSplitSdk({ config: sdkNodeConfig }); + (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); + + const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true }; + expect(getStatus()).toEqual(MAIN_CLIENT_STATUS); + expect(getStatus('ignored_key_in_server_side')).toEqual(MAIN_CLIENT_STATUS); + }); +}); diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index a6ff13b..9641701 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -10,6 +10,8 @@ import { getSplits as exportedGetSplits, selectTreatmentValue as exportedSelectTreatmentValue, selectTreatmentWithConfig as exportedSelectTreatmentWithConfig, + selectTreatmentAndStatus as exportedSelectTreatmentAndStatus, + selectTreatmentWithConfigAndStatus as exportedSelectTreatmentWithConfigAndStatus, connectSplit as exportedConnectSplit, connectToggler as exportedConnectToggler, mapTreatmentToProps as exportedMapTreatmentToProps, @@ -21,7 +23,7 @@ import { import { splitReducer } from '../reducer'; import { initSplitSdk, getTreatments, destroySplitSdk, splitSdk } from '../asyncActions'; import { track, getSplitNames, getSplit, getSplits } from '../helpers'; -import { selectTreatmentValue, selectTreatmentWithConfig } from '../selectors'; +import { selectTreatmentValue, selectTreatmentWithConfig, selectTreatmentAndStatus, selectTreatmentWithConfigAndStatus } from '../selectors'; import { connectSplit } from '../react-redux/connectSplit'; import { connectToggler, mapTreatmentToProps, mapIsFeatureOnToProps } from '../react-redux/connectToggler'; @@ -38,6 +40,8 @@ it('index should export modules', () => { expect(exportedGetSplits).toBe(getSplits); expect(exportedSelectTreatmentValue).toBe(selectTreatmentValue); expect(exportedSelectTreatmentWithConfig).toBe(selectTreatmentWithConfig); + expect(exportedSelectTreatmentAndStatus).toBe(selectTreatmentAndStatus); + expect(exportedSelectTreatmentWithConfigAndStatus).toBe(selectTreatmentWithConfigAndStatus); expect(exportedConnectSplit).toBe(connectSplit); expect(exportedConnectToggler).toBe(connectToggler); expect(exportedMapTreatmentToProps).toBe(mapTreatmentToProps); diff --git a/src/__tests__/selectors.test.ts b/src/__tests__/selectors.test.ts index f2a1fbf..0583e9b 100644 --- a/src/__tests__/selectors.test.ts +++ b/src/__tests__/selectors.test.ts @@ -35,7 +35,7 @@ describe('selectTreatmentValue', () => { expect(selectTreatmentValue(STATE_READY.splitio, SPLIT_INVALID, USER_1, 'some_value')).toBe('some_value'); }); - it('returns "control" and log error if the given splitState is invalid', () => { + it('returns "control" and logs error if the given splitState is invalid', () => { const errorSpy = jest.spyOn(console, 'error'); expect(selectTreatmentValue((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL); expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); @@ -58,14 +58,14 @@ describe('selectTreatmentWithConfig', () => { expect(selectTreatmentWithConfig(STATE_READY.splitio, SPLIT_INVALID, USER_1)).toBe(CONTROL_WITH_CONFIG); }); - it('returns the passed default treatment insteaad of "control" if the given feature flag name or key are invalid', () => { + it('returns the passed default treatment instead of "control" if the given feature flag name or key are invalid', () => { const DEFAULT_TREATMENT = { treatment: 'some_value', config: 'some_config' }; expect(selectTreatmentWithConfig(STATE_READY.splitio, SPLIT_1, USER_INVALID, DEFAULT_TREATMENT)).toBe(DEFAULT_TREATMENT); expect(selectTreatmentWithConfig(STATE_READY.splitio, SPLIT_INVALID, USER_1, DEFAULT_TREATMENT)).toBe(DEFAULT_TREATMENT); }); - it('returns "control" and log error if the given splitState is invalid', () => { + it('returns "control" and logs error if the given splitState is invalid', () => { const errorSpy = jest.spyOn(console, 'error'); expect(selectTreatmentWithConfig((STATE_READY as unknown as ISplitState), SPLIT_1, USER_INVALID)).toBe(CONTROL_WITH_CONFIG); expect(errorSpy).toBeCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); diff --git a/src/__tests__/selectorsWithStatus.test.ts b/src/__tests__/selectorsWithStatus.test.ts new file mode 100644 index 0000000..bf3a7d6 --- /dev/null +++ b/src/__tests__/selectorsWithStatus.test.ts @@ -0,0 +1,100 @@ +/** Mocks */ +import { SPLIT_1, SPLIT_2, STATE_READY, USER_1 } from './utils/storeState'; +import { mockSdk, Event } from './utils/mockBrowserSplitSdk'; +jest.mock('@splitsoftware/splitio', () => { + return { SplitFactory: mockSdk() }; +}); + +import mockStore from './utils/mockStore'; +import { STATE_INITIAL, STATUS_INITIAL } from './utils/storeState'; +import { sdkBrowserConfig } from './utils/sdkConfigs'; +import { initSplitSdk, getTreatments, splitSdk } from '../asyncActions'; + +/** Constants */ +import { ON, CONTROL, CONTROL_WITH_CONFIG, ERROR_SELECTOR_NO_SPLITSTATE } from '../constants'; + +/** Test targets */ +import { + selectTreatmentAndStatus, + selectTreatmentWithConfigAndStatus +} from '../selectors'; + +describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () => { + + const errorSpy = jest.spyOn(console, 'error'); + + beforeEach(() => { + errorSpy.mockClear(); + }); + + it('if Split state is invalid or SDK was not initialized, returns default treatment and initial status', () => { + const DEFAULT_TREATMENT = { treatment: 'some_value', config: 'some_config' }; + + expect(selectTreatmentWithConfigAndStatus({} as any, SPLIT_1, USER_1, DEFAULT_TREATMENT)).toEqual({ + treatment: DEFAULT_TREATMENT, + ...STATUS_INITIAL, + }); + expect(errorSpy).toHaveBeenCalledWith(ERROR_SELECTOR_NO_SPLITSTATE); + errorSpy.mockClear(); + + expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'default_value')).toEqual({ + treatment: 'default_value', + ...STATUS_INITIAL, + }); + expect(errorSpy).not.toHaveBeenCalled(); + }); + + it('if getTreatments action was not dispatched for the provided feature flag and key, returns default treatment and client status', () => { + const store = mockStore(STATE_INITIAL); + store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); + (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); + + expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1)).toEqual({ + treatment: CONTROL, + // status of main client: + ...STATUS_INITIAL, isReady: true, isOperational: true, + }); + + expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({ + treatment: 'some_value', + // USER_1 client has not been initialized yet: + ...STATUS_INITIAL, + }); + + store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); + (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); + + expect(selectTreatmentWithConfigAndStatus(STATE_INITIAL.splitio, SPLIT_2, USER_1)).toEqual({ + treatment: CONTROL_WITH_CONFIG, + // status of shared client: + ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, + }); + + expect(errorSpy).not.toHaveBeenCalled(); + }); + + it('happy path: returns the treatment value and status of the client', () => { + // The following actions result in STATE_READY state: + const store = mockStore(); + store.dispatch(initSplitSdk({ config: sdkBrowserConfig })); + (splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY); + (splitSdk.factory as any).client(USER_1).__emitter__.emit(Event.SDK_READY_FROM_CACHE); + store.dispatch(getTreatments({ splitNames: [SPLIT_1] })); + store.dispatch(getTreatments({ key: USER_1, splitNames: [SPLIT_2] })); + + expect(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1)).toEqual({ + treatment: ON, + ...STATUS_INITIAL, + isReady: true, isOperational: true, + }); + + expect(selectTreatmentWithConfigAndStatus(STATE_READY.splitio, SPLIT_2, USER_1)).toEqual({ + treatment: STATE_READY.splitio.treatments[SPLIT_2][USER_1], + ...STATUS_INITIAL, + isReadyFromCache: true, isOperational: true, + }); + + expect(errorSpy).not.toHaveBeenCalled(); + }); + +}); diff --git a/src/__tests__/utils/mockStore.ts b/src/__tests__/utils/mockStore.ts index 48c720c..f1b0906 100644 --- a/src/__tests__/utils/mockStore.ts +++ b/src/__tests__/utils/mockStore.ts @@ -4,6 +4,7 @@ import configureMockStore from 'redux-mock-store'; const middlewares: any[] = [thunk]; /** - * Utils to not call requires files every time that we need mock the store + * redux-mock-store is designed to test the action-related logic, not the reducer-related one. In other words, it does not update the Redux store. + * Use storeState.ts for mocks of the Redux store state. */ export default configureMockStore(middlewares); diff --git a/src/__tests__/utils/storeState.ts b/src/__tests__/utils/storeState.ts index e0c6cf1..b4af869 100644 --- a/src/__tests__/utils/storeState.ts +++ b/src/__tests__/utils/storeState.ts @@ -9,6 +9,23 @@ export const USER_1 = 'user_1'; export const USER_2 = 'user_2'; export const USER_INVALID = 'user_invalid'; +export const STATUS_INITIAL = { + isReady: false, + isReadyFromCache: false, + hasTimedout: false, + isDestroyed: false, +}; + +export const STATE_INITIAL: { splitio: ISplitState } = { + splitio: { + ...STATUS_INITIAL, + isTimedout: false, + lastUpdate: 0, + treatments: { + }, + }, +}; + export const STATE_READY: { splitio: ISplitState } = { splitio: { isReady: true, @@ -27,16 +44,3 @@ export const STATE_READY: { splitio: ISplitState } = { }, }, }; - -export const STATE_INITIAL: { splitio: ISplitState } = { - splitio: { - isReady: false, - isReadyFromCache: false, - isTimedout: false, - hasTimedout: false, - isDestroyed: false, - lastUpdate: 0, - treatments: { - }, - }, -}; diff --git a/src/asyncActions.ts b/src/asyncActions.ts index 7a3b828..ba6651c 100644 --- a/src/asyncActions.ts +++ b/src/asyncActions.ts @@ -4,7 +4,7 @@ import { Dispatch, Action } from 'redux'; import { IInitSplitSdkParams, IGetTreatmentsParams, IDestroySplitSdkParams, ISplitFactoryBuilder } from './types'; import { splitReady, splitReadyWithEvaluations, splitReadyFromCache, splitReadyFromCacheWithEvaluations, splitTimedout, splitUpdate, splitUpdateWithEvaluations, splitDestroy, addTreatments } from './actions'; import { VERSION, ERROR_GETT_NO_INITSPLITSDK, ERROR_DESTROY_NO_INITSPLITSDK, getControlTreatmentsWithConfig } from './constants'; -import { matching, getStatus, validateGetTreatmentsParams } from './utils'; +import { matching, __getStatus, validateGetTreatmentsParams } from './utils'; /** * Internal object SplitSdk. This object should not be accessed or @@ -64,7 +64,7 @@ export function initSplitSdk(params: IInitSplitSdkParams): (dispatch: Dispatch): Promise => { - const status = getStatus(defaultClient); + const status = __getStatus(defaultClient); if (status.hasTimedout) dispatch(splitTimedout()); // dispatched before `splitReady`, since it overwrites `isTimedout` property if (status.isReady) dispatch(splitReady()); @@ -141,7 +141,7 @@ export function getTreatments(params: IGetTreatmentsParams): Action | (() => voi }); } - const status = getStatus(client); + const status = __getStatus(client); // If the SDK is not ready, it stores the action to execute when ready if (!status.isReady) { diff --git a/src/constants.ts b/src/constants.ts index c62d723..a2d00cb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -42,15 +42,17 @@ export const SPLIT_DESTROY = 'SPLIT_DESTROY'; export const ADD_TREATMENTS = 'ADD_TREATMENTS'; // Warning and error messages -export const ERROR_GETT_NO_INITSPLITSDK = '[ERROR] To use "getTreatments" the SDK must be first initialized with an "initSplitSdk" action'; +const errorNoInitSplitSdk = (action: string) => `[ERROR] To use ${action} the SDK must be first initialized with an "initSplitSdk" action`; -export const ERROR_DESTROY_NO_INITSPLITSDK = '[ERROR] To use "destroySplitSdk" the SDK must be first initialized with an "initSplitSdk" action'; +export const ERROR_GETT_NO_INITSPLITSDK = errorNoInitSplitSdk('"getTreatments"'); -export const ERROR_TRACK_NO_INITSPLITSDK = '[ERROR] To use "track" the SDK must be first initialized with an "initSplitSdk" action'; +export const ERROR_DESTROY_NO_INITSPLITSDK = errorNoInitSplitSdk('"destroySplitSdk"'); -export const ERROR_MANAGER_NO_INITSPLITSDK = '[ERROR] To use the manager, the SDK must be first initialized with an "initSplitSdk" action'; +export const ERROR_TRACK_NO_INITSPLITSDK = errorNoInitSplitSdk('"track"'); -export const ERROR_SELECTOR_NO_SPLITSTATE = '[ERROR] When using selectors, "splitState" value must be a proper splitio piece of state'; +export const ERROR_MANAGER_NO_INITSPLITSDK = errorNoInitSplitSdk('the manager'); + +export const ERROR_SELECTOR_NO_SPLITSTATE = '[ERROR] To use selectors, "splitState" param must be a proper splitio piece of state'; export const ERROR_GETT_NO_PARAM_OBJECT = '[ERROR] "getTreatments" must be called with a param object containing a valid splitNames or flagSets properties'; diff --git a/src/helpers.ts b/src/helpers.ts index 0693b3e..1b35fcf 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,7 @@ import { splitSdk, getClient } from './asyncActions'; -import { ITrackParams } from './types'; +import { IStatus, ITrackParams } from './types'; import { ERROR_TRACK_NO_INITSPLITSDK, ERROR_MANAGER_NO_INITSPLITSDK } from './constants'; +import { __getStatus, matching } from './utils'; /** * This function track events, i.e., it invokes the actual `client.track*` methods. @@ -36,7 +37,7 @@ export function track(params: ITrackParams): boolean { } /** - * Get the array of feature flag names. + * Gets the array of feature flag names. * * @returns {string[]} The list of feature flag names. The list might be empty if the SDK was not initialized or if it's not ready yet. * @@ -52,7 +53,7 @@ export function getSplitNames(): string[] { } /** - * Get the data of a split in SplitView format. + * Gets the data of a split in SplitView format. * * @param {string} featureFlagName The name of the split we wan't to get info of. * @returns {SplitView} The SplitIO.SplitView of the given split, or null if split does not exist or the SDK was not initialized or is not ready. @@ -69,7 +70,7 @@ export function getSplit(featureFlagName: string): SplitIO.SplitView { } /** - * Get the array of feature flags data in SplitView format. + * Gets the array of feature flags data in SplitView format. * * @returns {SplitViews} The list of SplitIO.SplitView. The list might be empty if the SDK was not initialized or if it's not ready yet * @@ -83,3 +84,36 @@ export function getSplits(): SplitIO.SplitViews { return splitSdk.factory.manager().splits(); } + +/** + * Gets an object with the status properties of the SDK client or manager: + * + * - `isReady` indicates if the SDK client has emitted the SDK_READY event. + * - `isReadyFromCache` indicates if the SDK client has emitted the SDK_READY_FROM_CACHE event. + * - `hasTimedout` indicates if the SDK client has emitted the SDK_READY_TIMED_OUT event. + * - `isDestroyed` indicates if the SDK client has been destroyed, i.e., if the `destroySplitSdk` action was dispatched. + * + * @param {SplitIO.SplitKey} key To use only on client-side. Ignored in server-side. If a key is provided and a client associated to that key has been used, the status of that client is returned. + * If no key is provided, the status of the main client and manager is returned (the main client shares the status with the manager). + * + * @returns {IStatus} The status of the SDK client or manager. + * + * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#subscribe-to-events} + */ +export function getStatus(key?: SplitIO.SplitKey): IStatus { + if (splitSdk.factory) { + const stringKey = matching(key); + const isMainClient = splitSdk.isDetached || !stringKey || stringKey === matching((splitSdk.config as SplitIO.IBrowserSettings).core.key); + const client = isMainClient ? splitSdk.factory.client() : splitSdk.sharedClients[stringKey]; + + if (client) return __getStatus(client); + } + + // Default status if SDK is not initialized or client is not found. No warning logs for now, in case the helper is used before actions are dispatched + return { + isReady: false, + isReadyFromCache: false, + hasTimedout: false, + isDestroyed: false, + }; +} diff --git a/src/index.ts b/src/index.ts index dcd8012..c99eda8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ // For Redux export { splitReducer } from './reducer'; export { initSplitSdk, getTreatments, destroySplitSdk, splitSdk } from './asyncActions'; -export { track, getSplitNames, getSplit, getSplits } from './helpers'; -export { selectTreatmentValue, selectTreatmentWithConfig } from './selectors'; +export { track, getSplitNames, getSplit, getSplits, getStatus } from './helpers'; +export { selectTreatmentValue, selectTreatmentWithConfig, selectTreatmentAndStatus, selectTreatmentWithConfigAndStatus } from './selectors'; // For React-redux export { connectSplit } from './react-redux/connectSplit'; diff --git a/src/selectors.ts b/src/selectors.ts index 25ca3d0..c68064b 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -1,13 +1,16 @@ -import { ISplitState } from './types'; +import { ISplitState, IStatus } from './types'; import { CONTROL, CONTROL_WITH_CONFIG, DEFAULT_SPLIT_STATE_SLICE, ERROR_SELECTOR_NO_SPLITSTATE } from './constants'; import { matching } from './utils'; +import { getStatus } from './helpers'; export const getStateSlice = (sliceName: string) => (state: any) => state[sliceName]; export const defaultGetSplitState = getStateSlice(DEFAULT_SPLIT_STATE_SLICE); /** - * Selector function to extract a treatment evaluation from the Split state. It returns the treatment string value. + * This function extracts a treatment evaluation from the Split state. It returns the treatment string value. + * If a treatment is not found, it returns the default value, which is `'control'` if not specified. + * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * * @param {ISplitState} splitState * @param {string} featureFlagName @@ -19,12 +22,14 @@ export function selectTreatmentValue(splitState: ISplitState, featureFlagName: s } /** - * Selector function to extract a treatment evaluation from the Split state. It returns a treatment object containing its value and configuration. + * This function extracts a treatment evaluation from the Split state. It returns a treatment object containing its value and configuration. + * If a treatment is not found, it returns the default value, which is `{ treatment: 'control', configuration: null }` if not specified. + * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. * * @param {ISplitState} splitState * @param {string} featureFlagName * @param {SplitIO.SplitKey} key - * @param {TreatmentWithConfig} defaultValue + * @param {SplitIO.TreatmentWithConfig} defaultValue */ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): SplitIO.TreatmentWithConfig { const splitTreatments = splitState && splitState.treatments ? splitState.treatments[featureFlagName] : console.error(ERROR_SELECTOR_NO_SPLITSTATE); @@ -37,3 +42,44 @@ export function selectTreatmentWithConfig(splitState: ISplitState, featureFlagNa return treatment ? treatment : defaultValue; } + +/** + * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment string value and the status properties of the client: `isReady`, `isReadyFromCache`, `hasTimedout`, and `isDestroyed`. + * If a treatment is not found, it returns the default value, which is `'control'` if not specified. + * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. + * + * @param {ISplitState} splitState + * @param {string} featureFlagName + * @param {SplitIO.SplitKey} key + * @param {string} defaultValue + */ +export function selectTreatmentAndStatus(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: string = CONTROL): { + treatment: string +} & IStatus { + const result: any = selectTreatmentWithConfigAndStatus(splitState, featureFlagName, key, { treatment: defaultValue, config: null }); + result.treatment = result.treatment.treatment; + return result; +} + +/** + * This function extracts a treatment evaluation from the Split state. It returns an object that contains the treatment object and the status properties of the client: `isReady`, `isReadyFromCache`, `hasTimedout`, and `isDestroyed`. + * If a treatment is not found, it returns the default value as treatment, which is `{ treatment: 'control', configuration: null }` if not specified. + * A treatment is not found if an invalid Split state is passed or if a `getTreatments` action has not been dispatched for the provided feature flag name and key. + * + * @param {ISplitState} splitState + * @param {string} featureFlagName + * @param {SplitIO.SplitKey} key + * @param {SplitIO.TreatmentWithConfig} defaultValue + */ +export function selectTreatmentWithConfigAndStatus(splitState: ISplitState, featureFlagName: string, key?: SplitIO.SplitKey, defaultValue: SplitIO.TreatmentWithConfig = CONTROL_WITH_CONFIG): { + treatment: SplitIO.TreatmentWithConfig +} & IStatus { + const treatment = selectTreatmentWithConfig(splitState, featureFlagName, key, defaultValue); + + const status = getStatus(key); + + return { + ...status, + treatment, + }; +} diff --git a/src/types.ts b/src/types.ts index 499354b..6af78ec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,15 @@ -/** Type for Split reducer's slice of state */ -export interface ISplitState { +// @TODO Reuse IStatus and IClientStatus when interfaces are aligned (breaking change) +/** Type for Split client and manager status */ +export interface IStatus { /** - * isReady indicates if Split SDK is ready, i.e., if it has emitted an SDK_READY event. + * isReady indicates if Split client is ready, i.e., if it has emitted an SDK_READY event. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} */ isReady: boolean; /** - * isReadyFromCache indicates if Split SDK has emitted an SDK_READY_FROM_CACHE event, what means that the SDK is ready to + * isReadyFromCache indicates if Split client has emitted an SDK_READY_FROM_CACHE event, what means that the SDK is ready to * evaluate using LocalStorage cached data (which might be stale). * This flag only applies for the Browser if using LOCALSTORAGE as storage type. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} @@ -16,26 +17,30 @@ export interface ISplitState { isReadyFromCache: boolean; /** - * isTimedout indicates if the Split SDK has emitted an SDK_READY_TIMED_OUT event and is not ready. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} - */ - isTimedout: boolean; - - /** - * hasTimedout indicates if the Split SDK has ever emitted an SDK_READY_TIMED_OUT event. + * hasTimedout indicates if the Split client has ever emitted an SDK_READY_TIMED_OUT event. * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} */ hasTimedout: boolean; /** - * isDestroyed indicates if the Split SDK has been destroyed by dispatching a `destroySplitSdk` action. + * isDestroyed indicates if the Split client has been destroyed by dispatching a `destroySplitSdk` action. * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#shutdown} */ isDestroyed: boolean; +} + +/** Type for Split reducer's slice of state */ +export interface ISplitState extends IStatus { + + /** + * isTimedout indicates if the Split client has emitted an SDK_READY_TIMED_OUT event and is not ready. + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-subscribe-to-events-and-changes} + */ + isTimedout: boolean; /** - * lastUpdate is the timestamp of the last Split SDK event (SDK_READY, SDK_READY_TIMED_OUT or SDK_UPDATE). + * lastUpdate is the timestamp of the last Split client event (SDK_READY, SDK_READY_TIMED_OUT or SDK_UPDATE). * @see {@link https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK#advanced-subscribe-to-events-and-changes} */ lastUpdate: number; diff --git a/src/utils.ts b/src/utils.ts index 0a9b1f7..bdf915d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,7 +28,7 @@ export interface IClientStatus { isDestroyed: boolean; } -export function getStatus(client: SplitIO.IClient): IClientStatus { +export function __getStatus(client: SplitIO.IClient): IClientStatus { // @ts-expect-error, function exists but it is not part of JS SDK type definitions return client.__getStatus(); }