Skip to content

Commit da2ce96

Browse files
Merge pull request #119 from splitio/update_shared_clients_status
Update shared client status to store `isReadyFromCache` property
2 parents 31c5c76 + 4e6bb47 commit da2ce96

File tree

4 files changed

+39
-32
lines changed

4 files changed

+39
-32
lines changed

src/__tests__/asyncActions.browser.test.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -293,14 +293,13 @@ describe('getTreatments', () => {
293293
expect(store.getActions().length).toBe(4);
294294

295295
// getting the evaluation result and validating it matches the results from SDK calls
296-
const treatments = action.payload.treatments;
297296
expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenNthCalledWith(3, ['split1'], undefined);
298297
expect(splitSdk.factory.client().getTreatmentsWithConfig).toHaveBeenNthCalledWith(4, ['split2', 'split3'], attributes);
299298
const expectedTreatments = {
300299
...(splitSdk.factory.client().getTreatmentsWithConfig as jest.Mock).mock.results[2].value,
301300
...(splitSdk.factory.client().getTreatmentsWithConfig as jest.Mock).mock.results[3].value,
302301
};
303-
expect(treatments).toEqual(expectedTreatments);
302+
expect(action.payload.treatments).toEqual(expectedTreatments);
304303

305304
expect(splitSdk.factory.client().getTreatmentsWithConfig).toBeCalledTimes(4); // control assertion - getTreatmentsWithConfig calls
306305
expect(getClient(splitSdk).evalOnUpdate).toEqual({}); // control assertion - cbs scheduled for update
@@ -524,41 +523,42 @@ describe('getTreatments', () => {
524523
// Init SDK and set ready
525524
const store = mockStore(STATE_INITIAL);
526525
const actionResult = store.dispatch<any>(initSplitSdk({ config: sdkBrowserConfig }));
526+
(splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE);
527527
(splitSdk.factory as any).client().__emitter__.emit(Event.SDK_READY);
528528

529529
actionResult.then(() => {
530-
// SPLIT_READY should have been dispatched
531-
expect(store.getActions().length).toBe(1);
532-
let action = store.getActions()[0];
533-
expect(action).toEqual({
534-
type: SPLIT_READY,
535-
payload: {
536-
timestamp: expect.any(Number)
537-
}
538-
});
539-
540-
// If SDK is ready for the main key and a getTreatment is dispatched for a different user key:
541-
// the item is added to the 'evalOnReady' list of the new client,
530+
// SDK_READY_FROM_CACHE & SPLIT_READY should have been dispatched
531+
expect(store.getActions()).toEqual([{
532+
type: SPLIT_READY_FROM_CACHE, payload: { timestamp: expect.any(Number) }
533+
}, {
534+
type: SPLIT_READY, payload: { timestamp: expect.any(Number) }
535+
}]);
536+
537+
// If getTreatment is dispatched for a different user key, the item is added to the 'evalOnReady' list of the new client
542538
store.dispatch<any>(getTreatments({ splitNames: 'split2', key: 'other-user-key' }));
543539
expect(getClient(splitSdk).evalOnReady.length).toEqual(0); // control assertion - no evaluations were registered for SDK_READY on main client
544540
expect(getClient(splitSdk, 'other-user-key').evalOnReady.length).toEqual(1); // control assertion - 1 evaluation was registered for SDK_READY on the new client
545541
expect(getClient(splitSdk).evalOnUpdate).toEqual({});
546542

547-
// and an ADD_TREATMENTS action is dispatched with control treatments without calling SDK client.
548-
action = store.getActions()[1];
543+
// If SDK was ready from cache, the SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS action is dispatched for the new clients, calling SDK client to evaluate from cache
544+
let action = store.getActions()[2];
549545
expect(action).toEqual({
550-
type: ADD_TREATMENTS,
546+
type: SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS,
551547
payload: {
552548
key: 'other-user-key',
553-
treatments: getControlTreatmentsWithConfig(['split2'])
549+
timestamp: 0,
550+
treatments: expect.any(Object),
551+
nonDefaultKey: true
554552
}
555553
});
556-
expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toBeCalledTimes(0);
554+
555+
expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], undefined);
556+
expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments);
557557

558558
(splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_READY, 'other-user-key');
559559

560560
// The SPLIT_READY_WITH_EVALUATIONS action is dispatched synchronously once the SDK is ready for the new user key
561-
action = store.getActions()[2];
561+
action = store.getActions()[3];
562562
expect(action).toEqual({
563563
type: SPLIT_READY_WITH_EVALUATIONS,
564564
payload: {
@@ -570,16 +570,15 @@ describe('getTreatments', () => {
570570
});
571571

572572
// getting the evaluation result and validating it matches the results from SDK
573-
const treatments = action.payload.treatments;
574573
expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).lastCalledWith(['split2'], undefined);
575-
expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toHaveLastReturnedWith(treatments);
574+
expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toHaveLastReturnedWith(action.payload.treatments);
576575

577576
expect(getClient(splitSdk).evalOnUpdate).toEqual({}); // control assertion
578577

579578
// The getTreatments is dispatched again, but this time is evaluated with attributes and registered for 'evalOnUpdate'
580579
const attributes = { att1: 'att1' };
581580
store.dispatch<any>(getTreatments({ splitNames: 'split2', attributes, key: 'other-user-key', evalOnUpdate: true }));
582-
action = store.getActions()[3];
581+
action = store.getActions()[4];
583582
expect(action).toEqual({
584583
type: ADD_TREATMENTS,
585584
payload: {
@@ -592,7 +591,7 @@ describe('getTreatments', () => {
592591

593592
// The SPLIT_UPDATE_WITH_EVALUATIONS action is dispatched when the SDK is updated for the new user key
594593
(splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_UPDATE);
595-
action = store.getActions()[4];
594+
action = store.getActions()[5];
596595
expect(action).toEqual({
597596
type: SPLIT_UPDATE_WITH_EVALUATIONS,
598597
payload: {
@@ -608,7 +607,7 @@ describe('getTreatments', () => {
608607

609608
// We deregister the item from evalOnUpdate.
610609
store.dispatch<any>(getTreatments({ splitNames: 'split2', key: 'other-user-key', evalOnUpdate: false }));
611-
action = store.getActions()[5];
610+
action = store.getActions()[6];
612611
expect(action).toEqual({
613612
type: ADD_TREATMENTS,
614613
payload: {
@@ -622,7 +621,7 @@ describe('getTreatments', () => {
622621

623622
// Now, SDK_UPDATE events do not trigger SPLIT_UPDATE_WITH_EVALUATIONS but SPLIT_UPDATE instead
624623
(splitSdk.factory as any).client('other-user-key').__emitter__.emit(Event.SDK_UPDATE);
625-
action = store.getActions()[6];
624+
action = store.getActions()[7];
626625
expect(action).toEqual({
627626
type: SPLIT_UPDATE,
628627
payload: {
@@ -631,8 +630,8 @@ describe('getTreatments', () => {
631630
}
632631
});
633632

634-
expect(store.getActions().length).toBe(7); // control assertion - no more actions after the update.
635-
expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toBeCalledTimes(4); // control assertion - called 4 times, in actions SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, SPLIT_READY_WITH_EVALUATIONS, SPLIT_UPDATE_WITH_EVALUATIONS and ADD_TREATMENTS.
633+
expect(store.getActions().length).toBe(8); // control assertion - no more actions after the update.
634+
expect(splitSdk.factory.client('other-user-key').getTreatmentsWithConfig).toBeCalledTimes(5); // control assertion - called 5 times, in actions SPLIT_READY_FROM_CACHE_WITH_EVALUATIONS, SPLIT_READY_WITH_EVALUATIONS, ADD_TREATMENTS, SPLIT_UPDATE_WITH_EVALUATIONS and ADD_TREATMENTS.
636635

637636
done();
638637
});

src/__tests__/helpers.browser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ describe('getStatus', () => {
192192
(splitSdk.factory as any).client('user_2').__emitter__.emit(Event.SDK_READY_FROM_CACHE);
193193

194194
// Main client
195-
const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate };
195+
const MAIN_CLIENT_STATUS = { ...STATUS_INITIAL, isReadyFromCache: true, isReady: true, isOperational: true, lastUpdate: (splitSdk.factory.client() as any).__getStatus().lastUpdate };
196196
expect(getStatus()).toEqual(MAIN_CLIENT_STATUS);
197197
expect(getStatus(sdkBrowserConfig.core.key)).toEqual(MAIN_CLIENT_STATUS);
198198
expect(getStatus({ matchingKey: sdkBrowserConfig.core.key as string, bucketingKey: '' })).toEqual(MAIN_CLIENT_STATUS);

src/__tests__/utils/mockBrowserSplitSdk.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ export function mockSdk() {
3030

3131
return jest.fn((config: SplitIO.IBrowserSettings, __updateModules?: (modules: { settings: { version: string } }) => void) => {
3232

33+
// ATM, isReadyFromCache is shared among clients
34+
let isReadyFromCache = false;
35+
3336
function mockClient(key?: SplitIO.SplitKey) {
3437
// Readiness
3538
let isReady = false;
36-
let isReadyFromCache = false;
3739
let hasTimedout = false;
3840
let isDestroyed = false;
3941
let lastUpdate = 0;

src/asyncActions.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export function getTreatments(params: IGetTreatmentsParams): Action | (() => voi
148148
client.evalOnReady.push(params);
149149
}
150150

151+
// @TODO breaking: consider removing `evalOnReadyFromCache` config option, since `false` value has no effect on shared clients (they are ready from cache immediately) and on the main client if its ready from cache when `getTreatments` is called
151152
// If the SDK is not ready from cache and flag `evalOnReadyFromCache`, it stores the action to execute when ready from cache
152153
if (!status.isReadyFromCache && params.evalOnReadyFromCache) {
153154
client.evalOnReadyFromCache.push(params);
@@ -156,7 +157,12 @@ export function getTreatments(params: IGetTreatmentsParams): Action | (() => voi
156157
if (status.isOperational) {
157158
// If the SDK is operational (i.e., it is ready or ready from cache), it evaluates and adds treatments to the store
158159
const treatments = __getTreatments(client, [params]);
159-
return addTreatments(params.key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments);
160+
161+
// Shared clients might be ready from cache immediately, so we need to dispatch a single action that updates treatments and `isReadyFromCache` status atomically
162+
// @TODO handle this corner case by refactoring actions into a single action that includes both the client status and optional evaluation/s, to minimize state changes and avoid edge cases
163+
return status.isReadyFromCache && !status.isReady && !isMainClient(params.key) ?
164+
splitReadyFromCacheWithEvaluations(params.key, treatments, status.lastUpdate, true) :
165+
addTreatments(params.key || (splitSdk.config as SplitIO.IBrowserSettings).core.key, treatments);
160166
} else {
161167
// Otherwise, it adds control treatments to the store, without calling the SDK (no impressions sent)
162168
// With flag sets, an empty object is passed since we don't know their feature flag names
@@ -241,7 +247,7 @@ export function getClient(splitSdk: ISplitSdk, key?: SplitIO.SplitKey): IClientN
241247
if (splitSdk.dispatch) splitSdk.dispatch(splitTimedout(__getStatus(client).lastUpdate, key));
242248
});
243249

244-
// On SDK timed out, dispatch `splitReadyFromCache` action
250+
// On SDK ready from cache, dispatch `splitReadyFromCache` action
245251
client.once(client.Event.SDK_READY_FROM_CACHE, function onReadyFromCache() {
246252
if (!splitSdk.dispatch) return;
247253

0 commit comments

Comments
 (0)