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 <alas.eric@gmail.com>
This commit is contained in:
Eric Alas 2022-02-22 12:15:09 -05:00 коммит произвёл GitHub
Родитель 8b74c0d392
Коммит 5f7f5b864b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
3 изменённых файлов: 212 добавлений и 0 удалений

Просмотреть файл

@ -21,5 +21,8 @@ export interface IGlobalStore {
SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void, eager: boolean): () => void;
SubscribeToGlobalState(source: string, callback: (state: any) => void): () => void;
AddSelectors(source: string, selectors: Record<string, any>, mergeSelectors?: boolean): void;
SelectPartnerState(partner: string, selector: string, defaultReturn?: any): any;
SetLogger(logger: ILogger): void;
};

Просмотреть файл

@ -22,6 +22,7 @@ export class GlobalStore implements IGlobalStore {
private _eagerPartnerStoreSubscribers: { [key: string]: { [key: string]: (state) => void } }
private _eagerUnsubscribers: { [key: string]: { [key: string]: () => void } }
private _actionLogger: ActionLogger = null;
private _selectors: { [key: string]: any };
private constructor(private _logger: ILogger = null) {
this._stores = {};
@ -30,6 +31,7 @@ export class GlobalStore implements IGlobalStore {
this._eagerPartnerStoreSubscribers = {};
this._eagerUnsubscribers = {};
this._actionLogger = new ActionLogger(_logger);
this._selectors = {};
}
/**
@ -334,6 +336,48 @@ export class GlobalStore implements IGlobalStore {
this._actionLogger.SetLogger(logger);
}
/**
* 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.
*
* @access public
*
* @param {string} source Name of the application exposing an derived state API
* @param {Record<string, any>} selectors The collection of APIs of derived state selectors.
* @param {boolean} mergeSelectors If the source application already exposed an API set, merge the new API being passed in.
*
*/
AddSelectors(source: string, selectors: Record<string, any>, mergeSelectors = false) {
if (this._selectors[source] == undefined) {
this._selectors[source] = selectors;
}
if (this._selectors[source] != undefined && mergeSelectors) {
this._selectors[source] = Object.assign({}, this._selectors[source], selectors);
}
}
/**
* Summary: Select derived state from a partner app using the selector name
*
* @access public
*
* @param {string} partner Name of the partner application to select derived data from
* @param {string} selector The name of the API to select
* @param {any} defaultReturn If the partner app does not have that API exposed, return this default value instead of undefined.
*
*/
SelectPartnerState(partner: string, selector: string, defaultReturn?: any) {
if (this._selectors[partner] == undefined) {
throw new Error(`ERROR: ${partner} not exposed any selectors.`);
}
if (this._selectors[partner][selector] == undefined) {
console.warn(`${partner} has not exposed a selector with the name: ${selector}`);
return defaultReturn;
}
return this._selectors[partner][selector]();
}
private RegisterEagerSubscriptions(appName: string) {
let eagerCallbacksRegistrations = this._eagerPartnerStoreSubscribers[appName];
if (eagerCallbacksRegistrations === undefined || eagerCallbacksRegistrations === undefined)

Просмотреть файл

@ -480,4 +480,169 @@ describe("Global Store", () => {
expect(isGlobalStateChanged).toBe(true);
});
});
describe("AddSelectors", () => {
let dummyPartnerReducer: Reducer<any, any> = (state: string = "Default", action: IAction<any>) => {
switch (action.type) {
case "Local": return "Local";
case "Global": return "Global";
}
};
it("Should successfully expose derived state API", () => {
let partnerAppName = "SamplePartner-2022";
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
// Arrange
globalStore.AddSelectors(partnerAppName, {
selectStateUpperCased: () => {
const state = partnerStore.getState();
return state.toUpperCase()
},
selectStateLowerCased: () => {
const state = partnerStore.getState();
return state.toLowerCase()
}
});
// Assert
const api = (<any>globalStore)._selectors[partnerAppName];
expect(api.selectStateUpperCased).toBeDefined();
expect(api.selectStateLowerCased).toBeDefined();
});
it("Should successfully merge derived state API", () => {
let partnerAppName = "SamplePartner-2023";
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
// Arrange
globalStore.AddSelectors(partnerAppName, {
selectStateUpperCased: () => {
const state = partnerStore.getState();
return state.toUpperCase()
},
});
globalStore.AddSelectors(partnerAppName, {
selectStateLowerCased: () => {
const state = partnerStore.getState();
return state.toLowerCase()
}
}, true);
// Assert
const api = (<any>globalStore)._selectors[partnerAppName];
expect(api.selectStateUpperCased).toBeDefined();
expect(api.selectStateLowerCased).toBeDefined();
});
it("Should not merge derived state API", () => {
let partnerAppName = "SamplePartner-2024";
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
// Arrange
globalStore.AddSelectors(partnerAppName, {
selectStateUpperCased: () => {
const state = partnerStore.getState();
return state.toUpperCase()
},
});
globalStore.AddSelectors(partnerAppName, {
selectStateLowerCased: () => {
const state = partnerStore.getState();
return state.toLowerCase()
}
});
// Assert
const api = (<any>globalStore)._selectors[partnerAppName];
expect(api.selectStateUpperCased).toBeDefined();
expect(api.selectStateLowerCased).toBeUndefined();
})
})
describe("SelectPartnerState", () => {
let dummyPartnerReducer: Reducer<any, any> = (state: string = "Default", action: IAction<any>) => {
switch (action.type) {
case "Local": return "Local";
case "Global": return "Global";
default:
return state;
}
};
it("Should be able to request a piece of derived state with valid key", () => {
// Arrange
let partnerAppName = "SamplePartner-2012";
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
globalStore.AddSelectors(partnerAppName, {
selectStateUpperCased: () => {
const state = partnerStore.getState();
return state.toUpperCase()
},
selectStateLowerCased: () => {
const state = partnerStore.getState();
return state.toLowerCase()
}
});
// Act
const partnerStateComputedUpperCase = globalStore.SelectPartnerState(partnerAppName, "selectStateUpperCased");
expect(partnerStateComputedUpperCase).toEqual("DEFAULT");
const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased");
expect(partnerStateComputedLowerCase).toEqual("default");
});
it("Should be return undefined if derived state key is not defined", () => {
// Arrange
let partnerAppName = "SamplePartner-2013";
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
globalStore.AddSelectors(partnerAppName, {
selectStateUpperCased: () => {
const state = partnerStore.getState();
return state.toUpperCase()
},
});
// Act
const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased");
expect(partnerStateComputedLowerCase).toEqual(undefined);
});
it("Should be return default value if derived state key is not defined", () => {
// Arrange
let partnerAppName = "SamplePartner-2013";
const partnerStore = globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
globalStore.AddSelectors(partnerAppName, {
selectStateUpperCased: () => {
const state = partnerStore.getState();
return state.toUpperCase()
},
});
// Act
const partnerStateComputedLowerCase = globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased", "I am a default value");
expect(partnerStateComputedLowerCase).toEqual("I am a default value");
});
it("Should throw error if partner has not exposed any derived", () => {
// Arrange
let partnerAppName = "SamplePartner-2014";
let exceptionThrown = false;
globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
try {
globalStore.SelectPartnerState(partnerAppName, "selectStateLowerCased");
} catch {
exceptionThrown = true;
}
// Assert
expect(exceptionThrown).toBeTruthy();
});
});
});