feat: queue subscription callbacks to missing stores (eager registration) (#31)

* feat: eager store subscription. fixes #17
This commit is contained in:
Pratik Bhattacharya 2021-09-07 21:54:10 +05:30 коммит произвёл GitHub
Родитель 2707d49f9e
Коммит 35cfbd9a8a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 24276 добавлений и 340 удалений

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

@ -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",

8785
sample/counterApp/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -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",

6862
sample/shell/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

8790
sample/todoApp/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -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();