Skip to content

Commit

Permalink
Update tests and polishing
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilianoSanchez committed Sep 6, 2024
1 parent 29844ff commit cccc086
Show file tree
Hide file tree
Showing 13 changed files with 105 additions and 75 deletions.
6 changes: 6 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
1.14.0 (September XX, 2024)
- Added `lastUpdate` and `isTimedout` properties to the object returned by the `getStatus` helper and `selectTreatmentAndStatus` and `selectTreatmentWithConfigAndStatus` selectors, to expose the last event timestamp and the timedout status of the SDK clients (Related to https://github.com/splitio/redux-client/issues/113).
- Updated @splitsoftware/splitio package to version 10.28.0 that includes minor updates:
- Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks.
- Updated some transitive dependencies for vulnerability fixes.

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.
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
},
"homepage": "https://github.com/splitio/redux-client#readme",
"dependencies": {
"@splitsoftware/splitio": "10.28.0-rc.3",
"@splitsoftware/splitio": "10.28.0",
"tslib": "^2.3.1"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/helpers.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,13 @@ describe('getStatus', () => {
(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 };
const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate };
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 };
const USER_2_STATUS = { ...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, lastUpdate: (splitSdk.factory.client('user_2') as any).__getStatus().lastUpdate };
expect(getStatus('user_2')).toEqual(USER_2_STATUS);
expect(getStatus({ matchingKey: 'user_2', bucketingKey: '' })).toEqual(USER_2_STATUS);

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/helpers.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ describe('getStatus', () => {
initSplitSdk({ config: sdkNodeConfig });
(splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY);

const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true };
const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate };
expect(getStatus()).toEqual(MAIN_CLIENT_STATUS);
expect(getStatus('ignored_key_in_server_side')).toEqual(MAIN_CLIENT_STATUS);
});
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/selectorsWithStatus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () =>
expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1)).toEqual({
treatment: CONTROL,
// status of main client:
...STATUS_INITIAL, isReady: true, isOperational: true,
...STATUS_INITIAL, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate,
});

expect(selectTreatmentAndStatus(STATE_INITIAL.splitio, SPLIT_1, USER_1, 'some_value')).toEqual({
Expand All @@ -67,7 +67,7 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () =>
expect(selectTreatmentWithConfigAndStatus(STATE_INITIAL.splitio, SPLIT_2, USER_1)).toEqual({
treatment: CONTROL_WITH_CONFIG,
// status of shared client:
...STATUS_INITIAL, isReadyFromCache: true, isOperational: true,
...STATUS_INITIAL, isReadyFromCache: true, isOperational: true, lastUpdate: (splitSdk.factory.client(USER_1) as any).__getStatus().lastUpdate,
});

expect(errorSpy).not.toHaveBeenCalled();
Expand All @@ -85,13 +85,13 @@ describe('selectTreatmentAndStatus & selectTreatmentWithConfigAndStatus', () =>
expect(selectTreatmentAndStatus(STATE_READY.splitio, SPLIT_1)).toEqual({
treatment: ON,
...STATUS_INITIAL,
isReady: true, isOperational: true,
isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate
});

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,
isReadyFromCache: true, isOperational: true, lastUpdate: (splitSdk.factory.client(USER_1) as any).__getStatus().lastUpdate
});

expect(errorSpy).not.toHaveBeenCalled();
Expand Down
43 changes: 27 additions & 16 deletions src/__tests__/utils/mockBrowserSplitSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,22 @@ export function mockSdk() {

function mockClient(key?: SplitIO.SplitKey) {
// Readiness
let __isReady__: boolean | undefined;
let __isReadyFromCache__: boolean | undefined;
let __hasTimedout__: boolean | undefined;
let __isDestroyed__: boolean | undefined;
let isReady = false;
let isReadyFromCache = false;
let hasTimedout = false;
let isDestroyed = false;
let lastUpdate = 0;

function syncLastUpdate() {
const dateNow = Date.now();
lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1;
}

const __emitter__ = new EventEmitter();
__emitter__.once(Event.SDK_READY, () => { __isReady__ = true; });
__emitter__.once(Event.SDK_READY_FROM_CACHE, () => { __isReadyFromCache__ = true; });
__emitter__.once(Event.SDK_READY_TIMED_OUT, () => { __hasTimedout__ = true; });
__emitter__.once(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); });
__emitter__.once(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); });
__emitter__.once(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); });
__emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); });

// Client methods
const track: jest.Mock = jest.fn((tt, et, v, p) => {
Expand Down Expand Up @@ -77,19 +85,22 @@ export function mockSdk() {
});
const ready: jest.Mock = jest.fn(() => {
return promiseWrapper(new Promise<void>((res, rej) => {
__isReady__ ? res() : __emitter__.on(Event.SDK_READY, res);
__hasTimedout__ ? rej() : __emitter__.on(Event.SDK_READY_TIMED_OUT, rej);
isReady ? res() : __emitter__.on(Event.SDK_READY, res);
hasTimedout ? rej() : __emitter__.on(Event.SDK_READY_TIMED_OUT, rej);
}), () => { });
});
const __getStatus = () => ({
isReady: __isReady__ || false,
isReadyFromCache: __isReadyFromCache__ || false,
hasTimedout: __hasTimedout__ || false,
isDestroyed: __isDestroyed__ || false,
isOperational: ((__isReady__ || __isReadyFromCache__) && !__isDestroyed__) || false,
isReady,
isReadyFromCache,
isTimedout: hasTimedout && !isReady,
hasTimedout,
isDestroyed,
isOperational: (isReady || isReadyFromCache) && !isDestroyed,
lastUpdate,
});
const destroy: jest.Mock = jest.fn(() => {
__isDestroyed__ = true;
isDestroyed = true;
syncLastUpdate();
return new Promise((res) => { setTimeout(res, 100); });
});

Expand All @@ -105,7 +116,7 @@ export function mockSdk() {
getAttributes,
// EventEmitter exposed to trigger events manually
__emitter__,
// Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed)
// Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isTimedout, hasTimedout, isDestroyed, isOperational, lastUpdate)
__getStatus,
});
}
Expand Down
37 changes: 24 additions & 13 deletions src/__tests__/utils/mockNodeSplitSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ export const Event = {

function mockClient() {
// Readiness
let __isReady__: boolean | undefined;
let __hasTimedout__: boolean | undefined;
let __isDestroyed__: boolean | undefined;
let isReady = false;
let hasTimedout = false;
let isDestroyed = false;
let lastUpdate = 0;

function syncLastUpdate() {
const dateNow = Date.now();
lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1;
}

const __emitter__ = new EventEmitter();
__emitter__.once(Event.SDK_READY, () => { __isReady__ = true; });
__emitter__.once(Event.SDK_READY_TIMED_OUT, () => { __hasTimedout__ = true; });
__emitter__.once(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); });
__emitter__.once(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); });
__emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); });

// Client methods
const track: jest.Mock = jest.fn(() => {
Expand All @@ -35,19 +43,22 @@ function mockClient() {
});
const ready: jest.Mock = jest.fn(() => {
return promiseWrapper(new Promise<void>((res, rej) => {
__isReady__ ? res() : __emitter__.on(Event.SDK_READY, res);
__hasTimedout__ ? rej() : __emitter__.on(Event.SDK_READY_TIMED_OUT, rej);
isReady ? res() : __emitter__.on(Event.SDK_READY, res);
hasTimedout ? rej() : __emitter__.on(Event.SDK_READY_TIMED_OUT, rej);
}), () => { });
});
const __getStatus = () => ({
isReady: __isReady__ || false,
isReady,
isReadyFromCache: false,
hasTimedout: __hasTimedout__ || false,
isDestroyed: __isDestroyed__ || false,
isOperational: (__isReady__ && !__isDestroyed__) || false,
isTimedout: hasTimedout && !isReady,
hasTimedout,
isDestroyed,
isOperational: isReady && !isDestroyed,
lastUpdate,
});
const destroy: jest.Mock = jest.fn(() => {
__isDestroyed__ = true;
isDestroyed = true;
syncLastUpdate();
return new Promise((res) => { setTimeout(res, 100); });
});

Expand All @@ -60,7 +71,7 @@ function mockClient() {
Event,
// EventEmitter exposed to trigger events manually
__emitter__,
// Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed)
// Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isTimedout, hasTimedout, isDestroyed, isOperational, lastUpdate)
__getStatus,
});
}
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/utils/storeState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ export const USER_INVALID = 'user_invalid';
export const STATUS_INITIAL = {
isReady: false,
isReadyFromCache: false,
isTimedout: false,
hasTimedout: false,
isDestroyed: false,
lastUpdate: 0,
};

export const STATE_INITIAL: { splitio: ISplitState } = {
splitio: {
...STATUS_INITIAL,
isTimedout: false,
lastUpdate: 0,
treatments: {
},
},
Expand Down
8 changes: 2 additions & 6 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { splitSdk, getClient } from './asyncActions';
import { IStatus, ITrackParams } from './types';
import { ERROR_TRACK_NO_INITSPLITSDK, ERROR_MANAGER_NO_INITSPLITSDK } from './constants';
import { __getStatus, matching } from './utils';
import { initialStatus } from './reducer';

/**
* This function track events, i.e., it invokes the actual `client.track*` methods.
Expand Down Expand Up @@ -110,10 +111,5 @@ export function getStatus(key?: SplitIO.SplitKey): IStatus {
}

// 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,
};
return { ...initialStatus };
}
12 changes: 8 additions & 4 deletions src/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import {
SPLIT_UPDATE, SPLIT_UPDATE_WITH_EVALUATIONS, SPLIT_TIMEDOUT, SPLIT_DESTROY, ADD_TREATMENTS,
} from './constants';

/**
* Initial default state for Split reducer
*/
const initialState: ISplitState = {
export const initialStatus = {
isReady: false,
isReadyFromCache: false,
isTimedout: false,
hasTimedout: false,
isDestroyed: false,
lastUpdate: 0,
}

/**
* Initial default state for Split reducer
*/
const initialState: ISplitState = {
...initialStatus,
treatments: {},
};

Expand Down
20 changes: 10 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export interface IStatus {
*/
isReadyFromCache: boolean;

/**
* isTimedout indicates if the Split client has emitted an SDK_READY_TIMED_OUT event and is not ready.
* In other words, `isTimedout` is equivalent to `hasTimeout && !isReady`.
* @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 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.
Expand All @@ -28,23 +35,16 @@ export interface IStatus {
* @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 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;
}

/** Type for Split reducer's slice of state */
export interface ISplitState extends IStatus {
/**
* `treatments` is a nested object property that contains the evaluations of feature flags.
* Each evaluation (treatment) is associated with a feature flag name and a key (e.g., unique user identifier, such as a user id).
Expand Down
4 changes: 3 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ export function matching(key: SplitIO.SplitKey): string {
export interface IClientStatus {
isReady: boolean;
isReadyFromCache: boolean;
isOperational: boolean;
isTimedout: boolean;
hasTimedout: boolean;
isDestroyed: boolean;
isOperational: boolean;
lastUpdate: number;
}

export function __getStatus(client: SplitIO.IClient): IClientStatus {
Expand Down

0 comments on commit cccc086

Please sign in to comment.