Skip to content

Commit 5f7f5b8

Browse files
Add ability to expose derived state apis (#39)
* Add ability to expose derived state apis * rename API and arguments based on code review suggestions Co-authored-by: Eric Alas <[email protected]>
1 parent 8b74c0d commit 5f7f5b8

File tree

3 files changed

+212
-0
lines changed

3 files changed

+212
-0
lines changed

src/common/interfaces/global.store.interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ export interface IGlobalStore {
2121
SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void, eager: boolean): () => void;
2222
SubscribeToGlobalState(source: string, callback: (state: any) => void): () => void;
2323

24+
AddSelectors(source: string, selectors: Record<string, any>, mergeSelectors?: boolean): void;
25+
SelectPartnerState(partner: string, selector: string, defaultReturn?: any): any;
26+
2427
SetLogger(logger: ILogger): void;
2528
};

src/global.store.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class GlobalStore implements IGlobalStore {
2222
private _eagerPartnerStoreSubscribers: { [key: string]: { [key: string]: (state) => void } }
2323
private _eagerUnsubscribers: { [key: string]: { [key: string]: () => void } }
2424
private _actionLogger: ActionLogger = null;
25+
private _selectors: { [key: string]: any };
2526

2627
private constructor(private _logger: ILogger = null) {
2728
this._stores = {};
@@ -30,6 +31,7 @@ export class GlobalStore implements IGlobalStore {
3031
this._eagerPartnerStoreSubscribers = {};
3132
this._eagerUnsubscribers = {};
3233
this._actionLogger = new ActionLogger(_logger);
34+
this._selectors = {};
3335
}
3436

3537
/**
@@ -334,6 +336,48 @@ export class GlobalStore implements IGlobalStore {
334336
this._actionLogger.SetLogger(logger);
335337
}
336338

339+
/**
340+
* Summary: Expose a collection of Selecotrs from a Partner-level that other partners can later consume. This allows partners to derive data without forcing partners to know the state structure.
341+
*
342+
* @access public
343+
*
344+
* @param {string} source Name of the application exposing an derived state API
345+
* @param {Record<string, any>} selectors The collection of APIs of derived state selectors.
346+
* @param {boolean} mergeSelectors If the source application already exposed an API set, merge the new API being passed in.
347+
*
348+
*/
349+
AddSelectors(source: string, selectors: Record<string, any>, mergeSelectors = false) {
350+
if (this._selectors[source] == undefined) {
351+
this._selectors[source] = selectors;
352+
}
353+
354+
if (this._selectors[source] != undefined && mergeSelectors) {
355+
this._selectors[source] = Object.assign({}, this._selectors[source], selectors);
356+
}
357+
}
358+
359+
/**
360+
* Summary: Select derived state from a partner app using the selector name
361+
*
362+
* @access public
363+
*
364+
* @param {string} partner Name of the partner application to select derived data from
365+
* @param {string} selector The name of the API to select
366+
* @param {any} defaultReturn If the partner app does not have that API exposed, return this default value instead of undefined.
367+
*
368+
*/
369+
SelectPartnerState(partner: string, selector: string, defaultReturn?: any) {
370+
if (this._selectors[partner] == undefined) {
371+
throw new Error(`ERROR: ${partner} not exposed any selectors.`);
372+
}
373+
if (this._selectors[partner][selector] == undefined) {
374+
console.warn(`${partner} has not exposed a selector with the name: ${selector}`);
375+
return defaultReturn;
376+
}
377+
378+
return this._selectors[partner][selector]();
379+
}
380+
337381
private RegisterEagerSubscriptions(appName: string) {
338382
let eagerCallbacksRegistrations = this._eagerPartnerStoreSubscribers[appName];
339383
if (eagerCallbacksRegistrations === undefined || eagerCallbacksRegistrations === undefined)

test/global.store.tests.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,4 +480,169 @@ describe("Global Store", () => {
480480
expect(isGlobalStateChanged).toBe(true);
481481
});
482482
});
483+
484+
describe("AddSelectors", () => {
485+
let dummyPartnerReducer: Reducer<any, any> = (state: string = "Default", action: IAction<any>) => {
486+
switch (action.type) {
487+
case "Local": return "Local";
488+
case "Global": return "Global";
489+
}
490+
};
491+
492+
it("Should successfully expose derived state API", () => {
493+
let partnerAppName = "SamplePartner-2022";
494+
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
495+
496+
// Arrange
497+
globalStore.AddSelectors(partnerAppName, {
498+
selectStateUpperCased: () => {
499+
const state = partnerStore.getState();
500+
return state.toUpperCase()
501+
},
502+
selectStateLowerCased: () => {
503+
const state = partnerStore.getState();
504+
return state.toLowerCase()
505+
}
506+
});
507+
508+
// Assert
509+
const api = (<any>globalStore)._selectors[partnerAppName];
510+
expect(api.selectStateUpperCased).toBeDefined();
511+
expect(api.selectStateLowerCased).toBeDefined();
512+
});
513+
514+
it("Should successfully merge derived state API", () => {
515+
let partnerAppName = "SamplePartner-2023";
516+
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
517+
518+
// Arrange
519+
globalStore.AddSelectors(partnerAppName, {
520+
selectStateUpperCased: () => {
521+
const state = partnerStore.getState();
522+
return state.toUpperCase()
523+
},
524+
});
525+
526+
globalStore.AddSelectors(partnerAppName, {
527+
selectStateLowerCased: () => {
528+
const state = partnerStore.getState();
529+
return state.toLowerCase()
530+
}
531+
}, true);
532+
533+
// Assert
534+
const api = (<any>globalStore)._selectors[partnerAppName];
535+
expect(api.selectStateUpperCased).toBeDefined();
536+
expect(api.selectStateLowerCased).toBeDefined();
537+
});
538+
539+
it("Should not merge derived state API", () => {
540+
let partnerAppName = "SamplePartner-2024";
541+
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
542+
543+
// Arrange
544+
globalStore.AddSelectors(partnerAppName, {
545+
selectStateUpperCased: () => {
546+
const state = partnerStore.getState();
547+
return state.toUpperCase()
548+
},
549+
});
550+
551+
globalStore.AddSelectors(partnerAppName, {
552+
selectStateLowerCased: () => {
553+
const state = partnerStore.getState();
554+
return state.toLowerCase()
555+
}
556+
});
557+
558+
// Assert
559+
const api = (<any>globalStore)._selectors[partnerAppName];
560+
expect(api.selectStateUpperCased).toBeDefined();
561+
expect(api.selectStateLowerCased).toBeUndefined();
562+
})
563+
})
564+
565+
describe("SelectPartnerState", () => {
566+
let dummyPartnerReducer: Reducer<any, any> = (state: string = "Default", action: IAction<any>) => {
567+
switch (action.type) {
568+
case "Local": return "Local";
569+
case "Global": return "Global";
570+
default:
571+
return state;
572+
}
573+
};
574+
575+
it("Should be able to request a piece of derived state with valid key", () => {
576+
// Arrange
577+
let partnerAppName = "SamplePartner-2012";
578+
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
579+
580+
globalStore.AddSelectors(partnerAppName, {
581+
selectStateUpperCased: () => {
582+
const state = partnerStore.getState();
583+
return state.toUpperCase()
584+
},
585+
selectStateLowerCased: () => {
586+
const state = partnerStore.getState();
587+
return state.toLowerCase()
588+
}
589+
});
590+
591+
// Act
592+
const partnerStateComputedUpperCase = globalStore.SelectPartnerState(partnerAppName, "selectStateUpperCased");
593+
expect(partnerStateComputedUpperCase).toEqual("DEFAULT");
594+
const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased");
595+
expect(partnerStateComputedLowerCase).toEqual("default");
596+
});
597+
598+
it("Should be return undefined if derived state key is not defined", () => {
599+
// Arrange
600+
let partnerAppName = "SamplePartner-2013";
601+
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
602+
603+
globalStore.AddSelectors(partnerAppName, {
604+
selectStateUpperCased: () => {
605+
const state = partnerStore.getState();
606+
return state.toUpperCase()
607+
},
608+
});
609+
610+
// Act
611+
const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased");
612+
expect(partnerStateComputedLowerCase).toEqual(undefined);
613+
});
614+
615+
it("Should be return default value if derived state key is not defined", () => {
616+
// Arrange
617+
let partnerAppName = "SamplePartner-2013";
618+
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
619+
620+
globalStore.AddSelectors(partnerAppName, {
621+
selectStateUpperCased: () => {
622+
const state = partnerStore.getState();
623+
return state.toUpperCase()
624+
},
625+
});
626+
627+
// Act
628+
const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased", "I am a default value");
629+
expect(partnerStateComputedLowerCase).toEqual("I am a default value");
630+
});
631+
632+
it("Should throw error if partner has not exposed any derived", () => {
633+
// Arrange
634+
let partnerAppName = "SamplePartner-2014";
635+
let exceptionThrown = false;
636+
globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
637+
638+
try {
639+
globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased");
640+
} catch {
641+
exceptionThrown = true;
642+
}
643+
644+
// Assert
645+
expect(exceptionThrown).toBeTruthy();
646+
});
647+
});
483648
});

0 commit comments

Comments
 (0)