feat: queue subscription callbacks to missing stores (eager registration) (#31)
* feat: eager store subscription. fixes #17
This commit is contained in:
Родитель
2707d49f9e
Коммит
35cfbd9a8a
|
@ -52,11 +52,11 @@
|
|||
"@types/jasmine": "^3.5.14",
|
||||
"cpr": "^3.0.1",
|
||||
"jasmine": "^3.6.2",
|
||||
"karma": "^4.4.1",
|
||||
"karma": "^6.3.4",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-coverage": "^2.0.3",
|
||||
"karma-jasmine": "^3.3.1",
|
||||
"karma-typescript": "^4.1.1",
|
||||
"karma-typescript": "^5.5.1",
|
||||
"karma-typescript-es6-transform": "^4.1.1",
|
||||
"puppeteer": "^5.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -32,7 +32,6 @@
|
|||
"start-component": "webpack serve --config ./webpack.config.mf.js"
|
||||
},
|
||||
"private": false,
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.1",
|
||||
"@babel/core": "^7.12.3",
|
||||
|
@ -43,6 +42,7 @@
|
|||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^5.0.0",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"redux-micro-frontend": "^1.1.1",
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -18,6 +18,7 @@ export interface IGlobalStore {
|
|||
Subscribe(source: string, callback: (state: any) => void): () => void;
|
||||
SubscribeToPlatformState(source: string, callback: (state: any) => void): () => void;
|
||||
SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void): () => void;
|
||||
SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void, eager: boolean): () => void;
|
||||
SubscribeToGlobalState(source: string, callback: (state: any) => void): () => void;
|
||||
|
||||
SetLogger(logger: ILogger): void;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { IAction } from './actions/action.interface';
|
||||
import { ConsoleLogger } from './common/console.logger';
|
||||
import { ActionLogger } from './middlewares/action.logger';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import { AbstractLogger as ILogger } from './common/abstract.logger';
|
||||
import { IGlobalStore } from './common/interfaces/global.store.interface';
|
||||
import { Store, Reducer, Middleware, createStore, applyMiddleware } from 'redux';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
|
||||
/**
|
||||
* Summary Global store for all Apps and container shell (Platform) in Micro-Frontend application.
|
||||
|
@ -19,12 +19,16 @@ export class GlobalStore implements IGlobalStore {
|
|||
private _stores: { [key: string]: Store };
|
||||
private _globalActions: { [key: string]: Array<string> };
|
||||
private _globalListeners: Array<(state: any) => void>;
|
||||
private _eagerPartnerStoreSubscribers: { [key: string]: { [key: string]: (state) => void } }
|
||||
private _eagerUnsubscribers: { [key: string]: { [key: string]: () => void } }
|
||||
private _actionLogger: ActionLogger = null;
|
||||
|
||||
private constructor(private _logger: ILogger = null) {
|
||||
this._stores = {};
|
||||
this._globalActions = {};
|
||||
this._globalListeners = [];
|
||||
this._eagerPartnerStoreSubscribers = {};
|
||||
this._eagerUnsubscribers = {};
|
||||
this._actionLogger = new ActionLogger(_logger);
|
||||
}
|
||||
|
||||
|
@ -34,7 +38,7 @@ export class GlobalStore implements IGlobalStore {
|
|||
* @param {ILogger} logger Logger service.
|
||||
*/
|
||||
public static Get(debugMode: boolean = false, logger: ILogger = null): IGlobalStore {
|
||||
if(debugMode) {
|
||||
if (debugMode) {
|
||||
this.DebugMode = debugMode;
|
||||
}
|
||||
if (debugMode && (logger === undefined || logger === null)) {
|
||||
|
@ -66,7 +70,7 @@ export class GlobalStore implements IGlobalStore {
|
|||
if (existingStore === null || existingStore === undefined || shouldReplaceStore) {
|
||||
if (middlewares === undefined || middlewares === null)
|
||||
middlewares = [];
|
||||
let appStore = createStore(appReducer, GlobalStore.DebugMode ? composeWithDevTools( applyMiddleware(...middlewares)) : applyMiddleware(...middlewares));
|
||||
let appStore = createStore(appReducer, GlobalStore.DebugMode ? composeWithDevTools(applyMiddleware(...middlewares)) : applyMiddleware(...middlewares));
|
||||
this.RegisterStore(appName, appStore, globalActions, shouldReplaceStore);
|
||||
return appStore;
|
||||
}
|
||||
|
@ -96,6 +100,7 @@ export class GlobalStore implements IGlobalStore {
|
|||
this._stores[appName] = store;
|
||||
store.subscribe(this.InvokeGlobalListeners.bind(this));
|
||||
this.RegisterGlobalActions(appName, globalActions);
|
||||
this.RegisterEagerSubscriptions(appName);
|
||||
this.LogRegistration(appName, (existingStore !== undefined && existingStore !== null));
|
||||
}
|
||||
|
||||
|
@ -265,15 +270,29 @@ export class GlobalStore implements IGlobalStore {
|
|||
* @param {string} source Name of the application subscribing to the state changes.
|
||||
* @param {string} partner Name of the Partner application to whose store is getting subscribed to.
|
||||
* @param {(state: any) => void} callback Callback method to be called for every partner's state change.
|
||||
* @param {boolean} eager Allows subscription to store that's yet to registered
|
||||
*
|
||||
* @throws Error when the partner is yet to be registered/loaded or partner doesn't exist.
|
||||
*
|
||||
* @returns {() => void} Unsubscribe method. Call this method to unsubscribe to the changes.
|
||||
*/
|
||||
SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void): () => void {
|
||||
SubscribeToPartnerState(source: string, partner: string, callback: (state: any) => void, eager: boolean = true): () => void {
|
||||
let partnerStore = this.GetPartnerStore(partner);
|
||||
if (partnerStore === undefined || partnerStore === null) {
|
||||
throw new Error(`ERROR: ${source} is trying to subscribe to partner ${partner}. Either ${partner} doesn't exist or hasn't been loaded yet`);
|
||||
if (!eager) {
|
||||
throw new Error(`ERROR: ${source} is trying to subscribe to partner ${partner}. Either ${partner} doesn't exist or hasn't been loaded yet`);
|
||||
}
|
||||
if (this._eagerPartnerStoreSubscribers[partner]) {
|
||||
this._eagerPartnerStoreSubscribers[partner].source = callback;
|
||||
} else {
|
||||
this._eagerPartnerStoreSubscribers[partner] = {
|
||||
source: callback
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.UnsubscribeEagerSubscription(source, partner);
|
||||
}
|
||||
}
|
||||
return partnerStore.subscribe(() => callback(partnerStore.getState()));
|
||||
}
|
||||
|
@ -295,6 +314,18 @@ export class GlobalStore implements IGlobalStore {
|
|||
}
|
||||
}
|
||||
|
||||
UnsubscribeEagerSubscription(source: string, partnerName: string) {
|
||||
if (!partnerName || !source)
|
||||
return;
|
||||
|
||||
if (!this._eagerUnsubscribers[partnerName])
|
||||
return;
|
||||
|
||||
let unsubscriber = this._eagerUnsubscribers[partnerName].source;
|
||||
if (unsubscriber)
|
||||
unsubscriber();
|
||||
}
|
||||
|
||||
SetLogger(logger: ILogger) {
|
||||
if (this._logger === undefined || this._logger === null)
|
||||
this._logger = logger;
|
||||
|
@ -303,6 +334,26 @@ export class GlobalStore implements IGlobalStore {
|
|||
this._actionLogger.SetLogger(logger);
|
||||
}
|
||||
|
||||
private RegisterEagerSubscriptions(appName: string) {
|
||||
let eagerCallbacksRegistrations = this._eagerPartnerStoreSubscribers[appName];
|
||||
if (eagerCallbacksRegistrations === undefined || eagerCallbacksRegistrations === undefined)
|
||||
return;
|
||||
let registeredApps = Object.keys(eagerCallbacksRegistrations);
|
||||
registeredApps.forEach(sourceApp => {
|
||||
let callback = eagerCallbacksRegistrations[sourceApp];
|
||||
if (callback) {
|
||||
let unregistrationCallback = this.SubscribeToPartnerState(sourceApp, appName, callback, false);
|
||||
if (this._eagerPartnerStoreSubscribers[appName]) {
|
||||
this._eagerPartnerStoreSubscribers[appName].sourceApp = unregistrationCallback;
|
||||
} else {
|
||||
this._eagerPartnerStoreSubscribers[appName] = {
|
||||
sourceApp: unregistrationCallback
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private InvokeGlobalListeners(): void {
|
||||
let globalState = this.GetGlobalState();
|
||||
this._globalListeners.forEach(globalListener => {
|
||||
|
@ -325,9 +376,9 @@ export class GlobalStore implements IGlobalStore {
|
|||
|
||||
private IsActionRegisteredAsGlobal(appName: string, action: IAction<any>): boolean {
|
||||
let registeredGlobalActions = this._globalActions[appName];
|
||||
if (registeredGlobalActions === undefined || registeredGlobalActions === null) {
|
||||
return false;
|
||||
}
|
||||
if (registeredGlobalActions === undefined || registeredGlobalActions === null) {
|
||||
return false;
|
||||
}
|
||||
return registeredGlobalActions.some(registeredAction => registeredAction === action.type || registeredAction === GlobalStore.AllowAll);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { AbstractLogger as ILogger } from '../src/common/abstract.logger';
|
|||
describe("Global Store", () => {
|
||||
let mockLogger = {
|
||||
LogEvent: function (source, event, properties) { },
|
||||
LogException: function(source, error, properties) { }
|
||||
LogException: function (source, error, properties) { }
|
||||
} as ILogger;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -247,11 +247,11 @@ describe("Global Store", () => {
|
|||
|
||||
describe("DispatchGlobalAction", () => {
|
||||
let dummyPartnerReducer: Reducer<any, any> = (state: string = "Default", action: IAction<any>) => {
|
||||
switch(action.type) {
|
||||
switch (action.type) {
|
||||
case "Local": return "Local";
|
||||
case "Global": return "Global";
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
it("Should dispatch globally registered action on a partner store", () => {
|
||||
|
@ -294,15 +294,15 @@ describe("Global Store", () => {
|
|||
|
||||
describe("SubscribeToPartnerState", () => {
|
||||
let dummyPartnerReducer: Reducer<any, any> = (state: string = "Default", action: IAction<any>) => {
|
||||
switch(action.type) {
|
||||
switch (action.type) {
|
||||
case "Local": return "Local";
|
||||
case "Global": return "Global";
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
it("Should invoke callback when partner state changes", () => {
|
||||
// Arrange
|
||||
// Arrange
|
||||
let partnerAppName = "SamplePartner-40";
|
||||
let isPartnerStateChanged = false;
|
||||
globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
|
||||
|
@ -316,23 +316,108 @@ describe("Global Store", () => {
|
|||
type: "Global",
|
||||
payload: null
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Assert
|
||||
expect(isPartnerStateChanged).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should allow registering to non-registered store in eager mode", () => {
|
||||
// Arrange
|
||||
let partnerAppName = "SamplePartner-100";
|
||||
|
||||
// Act
|
||||
let exceptionThrown = false;
|
||||
try {
|
||||
globalStore.SubscribeToPartnerState("Test", partnerAppName, (state) => { }, true);
|
||||
} catch {
|
||||
exceptionThrown = true;
|
||||
}
|
||||
|
||||
// Assert
|
||||
expect(exceptionThrown).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should throw exception when registering to non-registered store in non-eager mode", () => {
|
||||
// Arrange
|
||||
let partnerAppName = "SamplePartner-101";
|
||||
|
||||
// Act
|
||||
let exceptionThrown = false;
|
||||
try {
|
||||
globalStore.SubscribeToPartnerState("Test", partnerAppName, (state) => { }, false);
|
||||
} catch {
|
||||
exceptionThrown = true;
|
||||
}
|
||||
|
||||
// Assert
|
||||
expect(exceptionThrown).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should attach eager subscriber", () => {
|
||||
// Arrange
|
||||
let partnerAppName = "SamplePartner-102";
|
||||
let isPartnerStateChanged = false;
|
||||
|
||||
// Act
|
||||
globalStore.SubscribeToPartnerState("Test", partnerAppName, (state) => {
|
||||
isPartnerStateChanged = true;
|
||||
}, true);
|
||||
|
||||
globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
|
||||
globalStore.DispatchGlobalAction("Test",
|
||||
{
|
||||
type: "Global",
|
||||
payload: null
|
||||
});
|
||||
|
||||
|
||||
// Assert
|
||||
expect(isPartnerStateChanged).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should attach eager multiple subscribers", () => {
|
||||
// Arrange
|
||||
let partnerAppName_1 = "SamplePartner-112";
|
||||
let isPartnerStateChanged_1 = false;
|
||||
|
||||
let partnerAppName_2 = "SamplePartner-114";
|
||||
let isPartnerStateChanged_2 = false;
|
||||
|
||||
// Act
|
||||
globalStore.SubscribeToPartnerState("Test", partnerAppName_1, (state) => {
|
||||
isPartnerStateChanged_1 = true;
|
||||
}, true);
|
||||
|
||||
globalStore.SubscribeToPartnerState("Test", partnerAppName_2, (state) => {
|
||||
isPartnerStateChanged_2 = true;
|
||||
}, true);
|
||||
|
||||
globalStore.CreateStore(partnerAppName_1, dummyPartnerReducer, [], ["Global"], false, false);
|
||||
globalStore.CreateStore(partnerAppName_2, dummyPartnerReducer, [], ["Global"], false, false);
|
||||
globalStore.DispatchGlobalAction("Test",
|
||||
{
|
||||
type: "Global",
|
||||
payload: null
|
||||
});
|
||||
|
||||
|
||||
// Assert
|
||||
expect(isPartnerStateChanged_1).toBeTruthy();
|
||||
expect(isPartnerStateChanged_2).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("SubscribeToGlobalState", () => {
|
||||
let dummyPartnerReducer: Reducer<any, any> = (state: string = "Default", action: IAction<any>) => {
|
||||
switch(action.type) {
|
||||
switch (action.type) {
|
||||
case "Local": return "Local";
|
||||
case "Global": return "Global";
|
||||
}
|
||||
};
|
||||
|
||||
it("Should invoke callback when global state changes due to partner change", () => {
|
||||
// Arrange
|
||||
// Arrange
|
||||
let partnerAppName = "SamplePartner-40";
|
||||
let isGlobalStateChanged = false;
|
||||
globalStore.CreateStore(partnerAppName, dummyPartnerReducer, [], ["Global"], false, false);
|
||||
|
@ -346,7 +431,7 @@ describe("Global Store", () => {
|
|||
type: "Global",
|
||||
payload: null
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Assert
|
||||
expect(isGlobalStateChanged).toBeTruthy();
|
||||
|
|
Загрузка…
Ссылка в новой задаче