From 3b203f2d1ff31691ed62dd7894747a6e41ec53f8 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 19 Sep 2024 07:06:26 +0200 Subject: [PATCH] Re-write ReduxStateSync lib in Frontend --- frontend/package-lock.json | 111 ++------ frontend/package.json | 2 +- frontend/src/features/SideWindow/index.tsx | 5 + frontend/src/libs/ReduxStateSync/constants.ts | 19 ++ frontend/src/libs/ReduxStateSync/index.ts | 249 +++++++----------- frontend/src/libs/ReduxStateSync/types.ts | 30 +++ frontend/src/libs/ReduxStateSync/utils.ts | 31 +++ frontend/src/store/index.ts | 18 +- 8 files changed, 216 insertions(+), 249 deletions(-) create mode 100644 frontend/src/libs/ReduxStateSync/constants.ts create mode 100644 frontend/src/libs/ReduxStateSync/types.ts create mode 100644 frontend/src/libs/ReduxStateSync/utils.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cf4cf43908..0244c059fd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,11 +12,11 @@ "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "6.0.1", "@mtes-mct/monitor-ui": "23.0.0", + "@paralleldrive/cuid2": "2.2.2", "@reduxjs/toolkit": "2.2.7", "@sentry/react": "7.117.0", "@tanstack/react-table": "8.20.5", "@tanstack/react-virtual": "3.10.7", - "broadcast-channel": "7.0.0", "comlink": "4.4.1", "date-fns": "3.6.0", "dayjs": "1.11.13", @@ -2882,6 +2882,18 @@ "styled-components": "^5.0.0 || ^6.0.0" } }, + "node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2920,6 +2932,15 @@ "node": ">= 8" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@petamoriken/float16": { "version": "3.8.7", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.7.tgz", @@ -6060,33 +6081,6 @@ "node": ">=8" } }, - "node_modules/broadcast-channel": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-7.0.0.tgz", - "integrity": "sha512-a2tW0Ia1pajcPBOGUF2jXlDnvE9d5/dg6BG9h60OmRUcZVr/veUrU8vEQFwwQIhwG3KVzYwSk3v2nRRGFgQDXQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "7.23.4", - "oblivious-set": "1.4.0", - "p-queue": "6.6.2", - "unload": "2.4.1" - }, - "funding": { - "url": "https://github.com/sponsors/pubkey" - } - }, - "node_modules/broadcast-channel/node_modules/@babel/runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", - "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/browserslist": { "version": "4.23.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", @@ -14358,15 +14352,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/oblivious-set": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.4.0.tgz", - "integrity": "sha512-szyd0ou0T8nsAqHtprRcP3WidfsN1TnAR5yWXf2mFCEr5ek3LEOkT6EZ/92Xfs74HIdyhG5WkGxIssMU0jBaeg==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/oidc-client-ts": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", @@ -14475,15 +14460,6 @@ "node": ">=12.20" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -14531,40 +14507,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue/node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -18276,15 +18218,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unload": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/unload/-/unload-2.4.1.tgz", - "integrity": "sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw==", - "license": "Apache-2.0", - "funding": { - "url": "https://github.com/sponsors/pubkey" - } - }, "node_modules/unplugin": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.12.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 54d95ab661..a591188668 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,11 +38,11 @@ "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "6.0.1", "@mtes-mct/monitor-ui": "23.0.0", + "@paralleldrive/cuid2": "2.2.2", "@reduxjs/toolkit": "2.2.7", "@sentry/react": "7.117.0", "@tanstack/react-table": "8.20.5", "@tanstack/react-virtual": "3.10.7", - "broadcast-channel": "7.0.0", "comlink": "4.4.1", "date-fns": "3.6.0", "dayjs": "1.11.13", diff --git a/frontend/src/features/SideWindow/index.tsx b/frontend/src/features/SideWindow/index.tsx index 1181e2afbb..03674bf334 100644 --- a/frontend/src/features/SideWindow/index.tsx +++ b/frontend/src/features/SideWindow/index.tsx @@ -10,6 +10,7 @@ import { Alert } from './Alert' import { BeaconMalfunctionBoard } from './BeaconMalfunctionBoard' import { BannerStack } from './components/BannerStack' import { Menu } from './Menu' +import { sideWindowActions } from './slice' import { useIsSuperUser } from '../../auth/hooks/useIsSuperUser' import { MissionEventContext } from '../../context/MissionEventContext' import { SideWindowMenuKey } from '../../domain/entities/sideWindow/constants' @@ -80,6 +81,10 @@ export function SideWindow() { dispatch(getAllCurrentReportings()) dispatch(getInfractions()) dispatch(getAllGearCodes()) + + window.addEventListener('beforeunload', () => { + dispatch(sideWindowActions.close()) + }) }, [dispatch]) return ( diff --git a/frontend/src/libs/ReduxStateSync/constants.ts b/frontend/src/libs/ReduxStateSync/constants.ts new file mode 100644 index 0000000000..3a2bb12ce3 --- /dev/null +++ b/frontend/src/libs/ReduxStateSync/constants.ts @@ -0,0 +1,19 @@ +import type { ReduxStateSyncOptions } from './types' + +export const BROADCAST_CHANNEL_NAME = 'monitorfish-channel' + +export const DEFAULT_OPTIONS: ReduxStateSyncOptions = { + actionFilter: () => true +} + +export enum MessageType { + DestroyTab = 'DestroyTab', + DispatchAction = 'DispatchAction', + GetInitialState = 'GetInitialState', + SendInitialState = 'SendInitialState' +} + +export enum InternalActionType { + InitializeStateFromOtherTab = '&_INITIALIZE_STATE_FROM_OTHER_TAB', + SendStateToOtherTab = '&_SEND_STATE_TO_OTHER_TAB' +} diff --git a/frontend/src/libs/ReduxStateSync/index.ts b/frontend/src/libs/ReduxStateSync/index.ts index c4df43f678..fda7da90d9 100644 --- a/frontend/src/libs/ReduxStateSync/index.ts +++ b/frontend/src/libs/ReduxStateSync/index.ts @@ -1,176 +1,127 @@ -import { BroadcastChannel, type BroadcastChannelOptions } from 'broadcast-channel' +import { createId } from '@paralleldrive/cuid2' +import { omit } from 'lodash' -import type { AnyObject } from '@mtes-mct/monitor-ui' -import type { MainAppDispatch } from '@store' - -let lastUuid = 0 -export const GET_INIT_STATE = '&_GET_INIT_STATE' -export const SEND_INIT_STATE = '&_SEND_INIT_STATE' -export const RECEIVE_INIT_STATE = '&_RECEIVE_INIT_STATE' -export const INIT_MESSAGE_LISTENER = '&_INIT_MESSAGE_LISTENER' - -const BROADCAST_CHANNEL_NAME = 'monitorfish-channel' +import { BROADCAST_CHANNEL_NAME, DEFAULT_OPTIONS, InternalActionType, MessageType } from './constants' +import { initializeStateFromOtherTab, sendStateToOtherTab, stampAction } from './utils' -interface ReduxStateSyncOptions { - actionFilter: (action: { type: string }) => boolean - broadcastChannelOptions: BroadcastChannelOptions - prepareState: (state: State) => State - receiveState: (_prevState: State, nextState: State) => State -} +import type { InternalAction, Message, ReduxStateSyncOptions, StampedAction } from './types' +import type { MainAppDispatch } from '@store' +import type { Action } from 'redux' + +class ReduxStateSync { + #channel: BroadcastChannel + #dispatch: MainAppDispatch | undefined + #isSynced: boolean + #tabId: string + #tabs: Record + + constructor() { + this.#channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME) + this.#isSynced = false + this.#tabId = createId() + this.#tabs = {} + + this.#channel.onmessage = this.#handleBroadcastChannelMessage.bind(this) + + window.addEventListener('beforeunload', () => { + this.#channel.postMessage({ + tabId: this.#tabId, + type: MessageType.DestroyTab + } satisfies Message) + }) + } -const DEFAULT_OPTIONS: ReduxStateSyncOptions = { - actionFilter: () => true, - broadcastChannelOptions: {}, - prepareState: state => state, - receiveState: (_prevState, nextState) => nextState -} + createStateSyncMiddleware = (options: Partial) => { + const controlledOptions = { ...DEFAULT_OPTIONS, ...options } -const getIniteState = () => ({ type: GET_INIT_STATE }) -const sendIniteState = () => ({ type: SEND_INIT_STATE }) -const receiveIniteState = state => ({ payload: state, type: RECEIVE_INIT_STATE }) -const initListener = () => ({ type: INIT_MESSAGE_LISTENER }) + return ({ dispatch, getState }) => + next => + (action: Action | InternalAction | StampedAction) => { + if (!this.#dispatch) { + this.#dispatch = dispatch -function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1) -} + this.#channel.postMessage({ + tabId: this.#tabId, + type: MessageType.GetInitialState + } satisfies Message) + } -function guid() { - return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}` -} + if ('$id' in action) { + if (action.$isReduxStateSyncAction) { + if (action.type === InternalActionType.SendStateToOtherTab) { + this.#channel.postMessage({ + state: omit(getState(), '_persist'), + tabId: this.#tabId, + type: MessageType.SendInitialState + } satisfies Message) + } + } -// generate current window unique id -export const WINDOW_STATE_SYNC_ID = guid() -// export for test -export function generateUuidForAction(action) { - const stampedAction = action - stampedAction.$uuid = guid() - stampedAction.$wuid = WINDOW_STATE_SYNC_ID + return next(action) + } - return stampedAction -} + const stampedAction = stampAction(action, this.#tabId) -function MessageListener( - this: any, - { - actionFilter, - channel, - dispatch - }: { - actionFilter: (action: { type: string }) => boolean - channel: BroadcastChannel - dispatch: MainAppDispatch - } -) { - let isSynced = false - const tabs = {} - this.handleOnMessage = stampedAction => { - // Ignore if this action is triggered by this window - if (stampedAction.$wuid === WINDOW_STATE_SYNC_ID) { - return - } - // IE bug https://stackoverflow.com/questions/18265556/why-does-internet-explorer-fire-the-window-storage-event-on-the-window-that-st - if (stampedAction.type === RECEIVE_INIT_STATE) { - return - } - // ignore other values that saved to localstorage. - if (stampedAction.$uuid && stampedAction.$uuid !== lastUuid) { - if (stampedAction.type === GET_INIT_STATE && !tabs[stampedAction.$wuid]) { - tabs[stampedAction.$wuid] = true - dispatch(sendIniteState()) - } else if (stampedAction.type === SEND_INIT_STATE && !tabs[stampedAction.$wuid]) { - if (!isSynced) { - isSynced = true - dispatch(receiveIniteState(stampedAction.payload)) + if (controlledOptions.actionFilter(stampedAction)) { + this.#channel.postMessage({ + stampedAction, + tabId: this.#tabId, + type: MessageType.DispatchAction + } satisfies Message) } - } else if (actionFilter(stampedAction)) { - lastUuid = stampedAction.$uuid - dispatch( - Object.assign(stampedAction, { - $isSync: true - }) - ) + + return next(stampedAction) } - } } - this.messageChannel = channel - this.messageChannel.onmessage = this.handleOnMessage -} -export const createStateSyncMiddleware = (options: Partial) => { - const controlledOptions = { ...DEFAULT_OPTIONS, ...options } - - const channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME, controlledOptions.broadcastChannelOptions) - const prepareState = options.prepareState ?? controlledOptions.prepareState - let messageListener = null - - return ({ dispatch, getState }) => - next => - action => { - if (!messageListener) { - messageListener = new MessageListener({ - actionFilter: controlledOptions.actionFilter, - channel, - dispatch - }) - } + #handleBroadcastChannelMessage(event: MessageEvent) { + const message = event.data - if (action && !action.$uuid) { - const stampedAction = generateUuidForAction(action) - lastUuid = stampedAction.$uuid - try { - if (action.type === SEND_INIT_STATE) { - if (getState()) { - stampedAction.payload = prepareState(getState()) + switch (message.type) { + case MessageType.GetInitialState: + if (!this.#tabs[message.tabId]) { + this.#tabs[message.tabId] = true - channel.postMessage(stampedAction) - } + this.#dispatch!(sendStateToOtherTab(message.tabId)) + } - return next(action) - } + return - if (controlledOptions.actionFilter(stampedAction) || action.type === GET_INIT_STATE) { - channel.postMessage(stampedAction) - } - } catch (e) { - console.error("Your browser doesn't support cross tab communication") + case MessageType.SendInitialState: + if (!this.#isSynced && message.state) { + this.#isSynced = true + + this.#dispatch!(initializeStateFromOtherTab(message.tabId, message.state)) } - } - return next( - Object.assign(action, { - $isSync: typeof action.$isSync === 'undefined' ? false : action.$isSync - }) - ) - } -} + return -// eslint-disable-next-line max-len -export const createReduxStateSync = - (appReducer, receiveState = DEFAULT_OPTIONS.receiveState) => - (state, action) => { - let initState = state - if (action.type === RECEIVE_INIT_STATE) { - initState = receiveState(state, action.payload) - } + case MessageType.DispatchAction: + if (message.stampedAction) { + this.#dispatch!(message.stampedAction) + } - return appReducer(initState, action) - } + // eslint-disable-next-line no-useless-return + return + + case MessageType.DestroyTab: + delete this.#tabs[message.tabId] -// init state with other tab's state -export const withReduxStateSync = createReduxStateSync + // eslint-disable-next-line no-useless-return + return -export const initStateWithPrevTab = ({ dispatch }) => { - dispatch(getIniteState()) + default: + break + } + } } -/* -if don't dispath any action, the store.dispath will not be available for message listener. -therefor need to trigger an empty action to init the messageListener. +export const reduxStateSync = new ReduxStateSync() + +export const withReduxStateSync = appReducer => (state, action: Action | InternalAction) => { + if ('payload' in action && action.type === InternalActionType.InitializeStateFromOtherTab) { + return appReducer(action.payload, action) + } -however, if already using initStateWithPrevTab, this function will be redundant -*/ -export const initMessageListener = ({ dispatch }) => { - dispatch(initListener()) + return appReducer(state, action) } diff --git a/frontend/src/libs/ReduxStateSync/types.ts b/frontend/src/libs/ReduxStateSync/types.ts new file mode 100644 index 0000000000..a6bb5007ca --- /dev/null +++ b/frontend/src/libs/ReduxStateSync/types.ts @@ -0,0 +1,30 @@ +import { MessageType, type InternalActionType } from './constants' + +import type { Action, ActionCreator as ReduxActionCreator } from 'redux' +import type { AnyObject } from 'yup' + +export type InternalActionCreator = ReduxActionCreator + +export interface ReduxStateSyncOptions { + actionFilter: (action: Action) => boolean +} + +export type Message = { + stampedAction?: StampedAction + state?: AnyObject + tabId: string + type: MessageType +} + +export type InternalAction = Action & { + $id: string + $isReduxStateSyncAction: true + $tabId: string + payload?: AnyObject +} + +export type StampedAction = Action & { + $id: string + $isReduxStateSyncAction: false + $tabId: string +} diff --git a/frontend/src/libs/ReduxStateSync/utils.ts b/frontend/src/libs/ReduxStateSync/utils.ts new file mode 100644 index 0000000000..29af475e84 --- /dev/null +++ b/frontend/src/libs/ReduxStateSync/utils.ts @@ -0,0 +1,31 @@ +import { createId } from '@paralleldrive/cuid2' + +import { InternalActionType } from './constants' + +import type { InternalAction, StampedAction } from './types' +import type { Action } from 'redux' +import type { AnyObject } from 'yup' + +export const sendStateToOtherTab = (tabId: string): InternalAction => ({ + $id: createId(), + $isReduxStateSyncAction: true, + $tabId: tabId, + type: InternalActionType.SendStateToOtherTab +}) + +export const initializeStateFromOtherTab = (tabId: string, state: AnyObject): InternalAction => ({ + $id: createId(), + $isReduxStateSyncAction: true, + $tabId: tabId, + payload: state, + type: InternalActionType.InitializeStateFromOtherTab +}) + +export function stampAction(action: Action, tabId: string): StampedAction { + return { + ...action, + $id: createId(), + $isReduxStateSyncAction: false, + $tabId: tabId + } +} diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 1cd05ba358..709b2a0fe4 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -5,8 +5,8 @@ * @see https://redux-toolkit.js.org/tutorials/rtk-query#add-the-service-to-your-store */ -import { createStateSyncMiddleware, initMessageListener } from '@libs/ReduxStateSync' -import { combineReducers, configureStore } from '@reduxjs/toolkit' +import { reduxStateSync, withReduxStateSync } from '@libs/ReduxStateSync' +import { combineReducers, configureStore, type Action } from '@reduxjs/toolkit' import { setupListeners } from '@reduxjs/toolkit/query' import { createTransform, FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE } from 'redux-persist' import persistReducer from 'redux-persist/es/persistReducer' @@ -19,7 +19,6 @@ import { monitorenvApi, monitorfishApi, monitorfishLightApi, monitorfishPublicAp import { mapToProcessingRegulation } from '../features/Regulation/utils' import type { RegulationState } from '../features/BackOffice/slice' -import type { AnyAction } from '@reduxjs/toolkit' import type { PersistConfig } from 'redux-persist' import type { ThunkAction } from 'redux-thunk' @@ -34,7 +33,7 @@ const persistedMainReducerConfig: PersistConfig = { } const persistedMainReducer = persistReducer( persistedMainReducerConfig, - combineReducers(mainReducer) as any + withReduxStateSync(combineReducers(mainReducer)) as any ) as unknown as typeof mainReducer export const mainStore = configureStore({ @@ -48,24 +47,23 @@ export const mainStore = configureStore({ monitorfishApi.middleware, monitorfishPublicApi.middleware, monitorfishLightApi.middleware, - createStateSyncMiddleware({ + reduxStateSync.createStateSyncMiddleware({ actionFilter: action => - !['persist/PERSIST'].includes(action.type) && + !action.type.startsWith('persist/') && !action.type.startsWith('monitorfishApi/') && !action.type.startsWith('monitorfishPublicApi/') - }) + }) as any ), reducer: persistedMainReducer }) setupListeners(mainStore.dispatch) -initMessageListener(mainStore) export const mainStorePersistor = persistStore(mainStore) // https://react-redux.js.org/using-react-redux/usage-with-typescript#define-root-state-and-dispatch-types // Infer the `MainRootState` and `AppDispatch` types from the store itself export type MainAppDispatch = typeof mainStore.dispatch -export type MainAppThunk = ThunkAction +export type MainAppThunk = ThunkAction export type MainRootState = ReturnType export type MainAppUseCase = () => MainAppThunk @@ -114,5 +112,5 @@ export const backofficeStorePersistor = persistStore(backofficeStore) // https://react-redux.js.org/using-react-redux/usage-with-typescript#define-root-state-and-dispatch-types export type BackofficeAppDispatch = typeof backofficeStore.dispatch -export type BackofficeAppThunk = ThunkAction +export type BackofficeAppThunk = ThunkAction export type BackofficeRootState = ReturnType