diff --git a/react-ui/docs/routine_pattern.md b/react-ui/docs/routine_pattern.md new file mode 100644 index 0000000000000000000000000000000000000000..97a29da31e704a64150a1723b51edc78b7e1ece2 --- /dev/null +++ b/react-ui/docs/routine_pattern.md @@ -0,0 +1,18 @@ +## Routine pattern +The goal is to get a generic architecture to invoke persist and rerun asynchronous actions (thunks). + + +### Goals +* Invoke asyncronous actions +* Rerun actions after page reload by persisting +* Ability to access and manipulate the redux state +* Give a simple interface to the outside world by abstracting the task to achive the upper goals + + +### Usage +Add + + +### Description +The image displays the whole workflow starting by the user that clicks a button. The actual routine pattern takes place within the red border. + diff --git a/react-ui/docs/routine_pattern.png b/react-ui/docs/routine_pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..86b582398523ed51922d8ebad316c15be59265db Binary files /dev/null and b/react-ui/docs/routine_pattern.png differ diff --git a/react-ui/src/components/subscriptions/action.subscription.ts b/react-ui/src/components/routines/action.routine.ts similarity index 100% rename from react-ui/src/components/subscriptions/action.subscription.ts rename to react-ui/src/components/routines/action.routine.ts diff --git a/react-ui/src/components/subscriptions/device.subscription.ts b/react-ui/src/components/routines/device.routine.ts similarity index 86% rename from react-ui/src/components/subscriptions/device.subscription.ts rename to react-ui/src/components/routines/device.routine.ts index 5afcc7c68d5c7be6aeb2549a5ef2e5dae0d113cd..0290f637d3cdfa20f9a2735ab93130377c670ee0 100644 --- a/react-ui/src/components/subscriptions/device.subscription.ts +++ b/react-ui/src/components/routines/device.routine.ts @@ -1,10 +1,10 @@ import { NetworkElementServiceGetAllFlattenedApiArg, api } from "@api/api"; -import { setDevices } from "@reducer/device.reducer"; +import { setDevices } from "@reducer/device.reducer/device.reducer"; import { setUser } from "@reducer/user.reducer"; import { createAsyncThunk } from "@reduxjs/toolkit"; import { RootState } from "src/stores"; -import { startListening } from "../../../src/stores/middleware/listener.middleware"; -import { FETCH_DEVICE_ACTION } from "./action.subscription"; +import { startListening } from "../../stores/middleware/listener.middleware"; +import { FETCH_DEVICE_ACTION } from "./action.routine"; // continously fetch devices const FETCH_DEVICES_INTERVAL = 15000; // in ms diff --git a/react-ui/src/components/subscriptions/index.ts b/react-ui/src/components/subscriptions/index.ts deleted file mode 100644 index 2a3e02f20022a66fa6d4c68dd814bd03e0c973aa..0000000000000000000000000000000000000000 --- a/react-ui/src/components/subscriptions/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AsyncThunk } from '@reduxjs/toolkit'; -import { fetchDevicesThunk } from './device.subscription'; -import { fetchSelectedMneThunk } from './mne.subscription'; - - -export enum THUNK_TYPE { - MNE = 'device/fetch', - DEVICE = 'mne/fetch', -} - -export interface SubscriptionThunkModule { - thunkFn?: AsyncThunk<any, any, {}> - type: THUNK_TYPE, -} - -export const SubscriptionThunks: SubscriptionThunkModule[] = [ - { - thunkFn: fetchDevicesThunk, - type: THUNK_TYPE.DEVICE - }, { - - thunkFn: fetchSelectedMneThunk, - type: THUNK_TYPE.MNE - } -] - diff --git a/react-ui/src/components/view/device/device.view.tsx b/react-ui/src/components/view/device/device.view.tsx index 0ef5c05b96ded16966d81393ab681e215b1fd5c5..c85ce6be6d52e1049ca663e3e0b0f597bd025a8d 100644 --- a/react-ui/src/components/view/device/device.view.tsx +++ b/react-ui/src/components/view/device/device.view.tsx @@ -1,10 +1,10 @@ import { useDeviceViewModel } from '@viewmodel/device.viewmodel'; import { useRef } from 'react'; -import { Button, Col, Container, FloatingLabel, Form, Nav, NavLink, Row } from 'react-bootstrap'; +import { Button, Col, Container, Form, Nav, NavLink, Row } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { DeviceViewTabs, DeviceViewTabValues } from './device.view.tabs'; import './device.scss'; import { DeviceViewTable } from './device.view.table'; +import { DeviceViewTabs, DeviceViewTabValues } from './device.view.tabs'; function DeviceView() { const { t } = useTranslation('common'); @@ -27,9 +27,9 @@ function DeviceView() { <Row className='align-items-center'> <Col sm={3}> - <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'> - <Form.Control type="text" placeholder={t('device.search.placeholder')} ref={searchRef} /> - </Form.Group> + <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'> + <Form.Control type="text" placeholder={t('device.search.placeholder')} ref={searchRef} /> + </Form.Group> </Col> <Col sm={{ span: 2, offset: 2 }} className='border-right pt-2'> <Button variant='primary' className='w-100 my-auto'>{t('device.add_device_button')}</Button> diff --git a/react-ui/src/components/view_model/device.table.viewmodel.ts b/react-ui/src/components/view_model/device.table.viewmodel.ts index 22d8ae9e0857891d20a6b5504c6c7df7165c44b2..0802d6b575c4240f6f0cb18be612e294d53e01cf 100644 --- a/react-ui/src/components/view_model/device.table.viewmodel.ts +++ b/react-ui/src/components/view_model/device.table.viewmodel.ts @@ -1,5 +1,5 @@ import { useAppDispatch } from "@hooks"; -import { Device, setSelectedDevice } from "@reducer/device.reducer"; +import { Device, setSelectedDevice } from "@reducer/device.reducer/device.reducer"; import { useEffect, useState } from "react"; export const useDeviceTableViewModel = (searchRef) => { diff --git a/react-ui/src/components/view_model/device.viewmodel.ts b/react-ui/src/components/view_model/device.viewmodel.ts index ec75927f033dd5d19b4779fd6d6fe6d000e6cd39..6dd01d07ede92c977e80702d2700a7b688e1107f 100644 --- a/react-ui/src/components/view_model/device.viewmodel.ts +++ b/react-ui/src/components/view_model/device.viewmodel.ts @@ -1,5 +1,5 @@ import { useAppDispatch, useAppSelector } from "@hooks"; -import { setActiveTab as setActiveTabState } from "@reducer/device.reducer"; +import { setActiveTab as setActiveTabState } from "@reducer/device.reducer/device.reducer"; import { DeviceViewTabValues } from "@view/device/device.view.tabs"; export const useDeviceViewModel = () => { diff --git a/react-ui/src/index.tsx b/react-ui/src/index.tsx index a2ddc81a9091427a9d3eaf25a711836dc6819de3..dddcf5c07296c768486fcd7cde1c612f7c127ac6 100644 --- a/react-ui/src/index.tsx +++ b/react-ui/src/index.tsx @@ -1,5 +1,5 @@ import React from 'react' -import ReactDOM, { Container } from 'react-dom/client' +import ReactDOM from 'react-dom/client' import { RouterProvider } from 'react-router-dom' @@ -12,8 +12,8 @@ import { ToastContainer } from 'react-toastify' import { PersistGate } from 'redux-persist/integration/react' import './i18n/config' import { router } from './routes' +import './shared/icons/icons' import { persistor, store } from './stores' -import './utils/icons/icons' const installToastify = () => { return ( @@ -34,4 +34,3 @@ ReactDOM.createRoot(document.getElementById("root")).render( </React.StrictMode> ); -import './components/subscriptions' \ No newline at end of file diff --git a/react-ui/src/routes.tsx b/react-ui/src/routes.tsx index b273246d9ca6d5067e2b38185036443a05a65676..a63d6d87552a05da1c1cebd6d4736f19ae1c96c9 100644 --- a/react-ui/src/routes.tsx +++ b/react-ui/src/routes.tsx @@ -1,8 +1,8 @@ -import { BasicLayout } from "@layout/basic.layout" -import { LoginLayout } from "@layout/login.layout" -import { ProtectedLayout } from "@layout/protected.layout/protected.layout" -import DeviceView from "@view/device/device.view" -import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom" +import { BasicLayout } from "@layout/basic.layout"; +import { LoginLayout } from "@layout/login.layout"; +import { ProtectedLayout } from "@layout/protected.layout/protected.layout"; +import DeviceView from "@view/device/device.view"; +import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom"; export const DEVICE_URL = '/device/'; export const LOGIN_URL = '/login'; diff --git a/react-ui/src/utils/api/api.ts b/react-ui/src/shared/api/api.ts similarity index 100% rename from react-ui/src/utils/api/api.ts rename to react-ui/src/shared/api/api.ts diff --git a/react-ui/src/utils/helper/coookie.ts b/react-ui/src/shared/helper/coookie.ts similarity index 100% rename from react-ui/src/utils/helper/coookie.ts rename to react-ui/src/shared/helper/coookie.ts diff --git a/react-ui/src/utils/icons/icons.ts b/react-ui/src/shared/icons/icons.ts similarity index 100% rename from react-ui/src/utils/icons/icons.ts rename to react-ui/src/shared/icons/icons.ts diff --git a/react-ui/src/utils/layouts/basic.layout.tsx b/react-ui/src/shared/layouts/basic.layout.tsx similarity index 88% rename from react-ui/src/utils/layouts/basic.layout.tsx rename to react-ui/src/shared/layouts/basic.layout.tsx index 7b7f2b11c1780101c70f71317a3e5ed8204cf5b1..640af3e1ba3948e61cf80977f865b4db9cd9a8ea 100644 --- a/react-ui/src/utils/layouts/basic.layout.tsx +++ b/react-ui/src/shared/layouts/basic.layout.tsx @@ -4,11 +4,11 @@ import { useOutlet } from "react-router-dom"; export const BasicLayout = () => { const outlet = useOutlet(); - + return ( <AuthProvider> - {outlet} + {outlet} </AuthProvider> ) } \ No newline at end of file diff --git a/react-ui/src/utils/layouts/login.layout.tsx b/react-ui/src/shared/layouts/login.layout.tsx similarity index 100% rename from react-ui/src/utils/layouts/login.layout.tsx rename to react-ui/src/shared/layouts/login.layout.tsx diff --git a/react-ui/src/utils/layouts/protected.layout/protected.layout.scss b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss similarity index 100% rename from react-ui/src/utils/layouts/protected.layout/protected.layout.scss rename to react-ui/src/shared/layouts/protected.layout/protected.layout.scss diff --git a/react-ui/src/utils/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx similarity index 98% rename from react-ui/src/utils/layouts/protected.layout/protected.layout.tsx rename to react-ui/src/shared/layouts/protected.layout/protected.layout.tsx index 588491230158d7053f51fa1a28e2151c8d23b8cc..faff61c40e62c67b32dddb699cf4fb50dbe4a6ea 100644 --- a/react-ui/src/utils/layouts/protected.layout/protected.layout.tsx +++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx @@ -1,16 +1,16 @@ import logo from '@assets/logo.svg'; import { faCircleUser, faRightFromBracket } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useAppDispatch, useAppSelector } from '@hooks'; import { useAuth } from "@provider/auth.provider"; +import { fetchPnds } from '@reducer/device.reducer/device.reducer'; +import { fetchUser } from '@reducer/user.reducer'; +import { DEVICE_URL, LOGIN_URL } from '@routes'; import React, { useEffect } from "react"; import { Dropdown } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { Link, Outlet, useNavigate } from "react-router-dom"; import "./protected.layout.scss"; -import { useAppDispatch, useAppSelector } from '@hooks'; -import { fetchUser } from '@reducer/user.reducer'; -import { fetchPnds } from '@reducer/device.reducer'; -import { DEVICE_URL, LOGIN_URL } from '@routes'; export const ProtectedLayout = () => { diff --git a/react-ui/src/utils/provider/auth.provider.tsx b/react-ui/src/shared/provider/auth.provider.tsx similarity index 100% rename from react-ui/src/utils/provider/auth.provider.tsx rename to react-ui/src/shared/provider/auth.provider.tsx diff --git a/react-ui/src/shared/utils/routine.manager.ts b/react-ui/src/shared/utils/routine.manager.ts new file mode 100644 index 0000000000000000000000000000000000000000..c86e30d4a6f8eeee961e62fbfd0b444a0440a837 --- /dev/null +++ b/react-ui/src/shared/utils/routine.manager.ts @@ -0,0 +1,88 @@ +import { fetchSelectedMneThunk } from '@reducer/device.reducer/mne.subscription'; +import { AsyncThunk } from '@reduxjs/toolkit'; +import { QueryActionCreatorResult } from '@reduxjs/toolkit/query'; +import { fetchDevicesThunk } from '@routine/device.routine'; + +type Routine = QueryActionCreatorResult<any>; + +interface Entity { + routine: Routine, + id: number +} + +const initialState = { + routines: [] as Entity[] +} + +export enum THUNK_KEY { + MNE = 'device/fetch', + DEVICE = 'mne/fetch', +} + +export const RoutineDictionary = new Map<THUNK_KEY,AsyncThunk<any, any, {}>>([ + [THUNK_KEY.DEVICE, fetchDevicesThunk], + [THUNK_KEY.MNE, fetchSelectedMneThunk] +]) + + +/** + * Routine manager is a singleton that holds all running routines. + * The redux store holds any persistable information about the routines. + * The routines objects itself are stored in the RoutineManager. + */ +export const RoutineManager = (() => { + let state = initialState; + const add = (routine: Routine): number => { + const id = state.routines.length; + + const newEntity: Entity = { + routine: routine, + id + } + + state.routines = [...state.routines, newEntity]; + + return id; + } + + + const unsubscribeAll = () => { + state.routines.forEach(({ routine: subscription }) => { + _unsubscribe(subscription) + }); + + state.routines = initialState.routines; + } + + /** + * @param id + * @returns returns true if the routine was stopped, false if it was not found + */ + const unsubscribe = (id: number): boolean => { + const routine = state.routines.find(({ id: routineId }) => routineId === id); + + if (routine) { + _unsubscribe(routine.routine); + } + + return !!routine; + } + + /** + * Actual unsubscribe process. + * This process is extracted to have a single process of unsubscribing. + * + * @param subscription + */ + const _unsubscribe = (subscription: Routine) => { + subscription.unsubscribe(); + // TODO remove from state + } + + + return { + add, + unsubscribe, + unsubscribeAll + } +})(); \ No newline at end of file diff --git a/react-ui/src/stores/index.ts b/react-ui/src/stores/index.ts index 8d2be362f62efa197e771cb2caaecd9798c4cc60..1b3c3335e60549d6bc6934698d099da759ebe46e 100644 --- a/react-ui/src/stores/index.ts +++ b/react-ui/src/stores/index.ts @@ -1,6 +1,6 @@ import { configureStore } from '@reduxjs/toolkit' import { setupListeners } from '@reduxjs/toolkit/query' -import { FETCH_DEVICE_ACTION, FETCH_MNE_ACTION } from '@subscription/action.subscription' +import { FETCH_DEVICE_ACTION, FETCH_MNE_ACTION } from '@routine/action.routine' import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE } from 'redux-persist' import persistStore from 'redux-persist/es/persistStore' import { emptySplitApi } from './api.store' @@ -14,7 +14,7 @@ export const store = configureStore({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { - ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, FETCH_DEVICE_ACTION + '/fulfilled', FETCH_MNE_ACTION + '/fulfilled'], + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, FETCH_DEVICE_ACTION + '/fulfilled', FETCH_MNE_ACTION + '/fulfilled', 'routine/addRoutine'], }, }).prepend(listenerMiddleware.middleware).concat(emptySplitApi.middleware, rtkQueryErrorLogger), }) diff --git a/react-ui/src/stores/persist.store.ts b/react-ui/src/stores/persist.store.ts index f14de1bd7f383e3f667a880293a34a52605c8c3e..65ade7d3e85184bef4d8ec10c66acab3e09bdf5a 100644 --- a/react-ui/src/stores/persist.store.ts +++ b/react-ui/src/stores/persist.store.ts @@ -1,5 +1,5 @@ -import deviceReducer from "@reducer/device.reducer"; -import subscriptionReducer from "@reducer/subscription.reducer"; +import deviceReducer from "@reducer/device.reducer/device.reducer"; +import routineReducer from "@reducer/routine.reducer"; import userReducer from "@reducer/user.reducer"; import { combineReducers } from "redux"; import { persistReducer } from "redux-persist"; @@ -18,7 +18,7 @@ const rootPersistConfig = { const rootReducer = combineReducers({ user: userReducer, device: deviceReducer, - subscription: subscriptionReducer, + routine: routineReducer, [emptySplitApi.reducerPath]: emptySplitApi.reducer, }) diff --git a/react-ui/src/stores/reducer/device.reducer.ts b/react-ui/src/stores/reducer/device.reducer/device.reducer.ts similarity index 97% rename from react-ui/src/stores/reducer/device.reducer.ts rename to react-ui/src/stores/reducer/device.reducer/device.reducer.ts index 5919062d27f4464dca20b03e78d2cf4ca4d3148c..9f21bcbeb7273bdbb3cab53ff8749bee7f7eeaa3 100644 --- a/react-ui/src/stores/reducer/device.reducer.ts +++ b/react-ui/src/stores/reducer/device.reducer/device.reducer.ts @@ -1,7 +1,6 @@ import { api, NetworkelementFlattenedManagedNetworkElement, NetworkelementManagedNetworkElement, PndPrincipalNetworkDomain, PndServiceGetPndListApiArg } from '@api/api'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { DeviceViewTabValues } from '@view/device/device.view.tabs'; -import { startListening } from '../middleware/listener.middleware'; export type Device = NetworkelementFlattenedManagedNetworkElement; diff --git a/react-ui/src/components/subscriptions/mne.subscription.ts b/react-ui/src/stores/reducer/device.reducer/mne.subscription.ts similarity index 75% rename from react-ui/src/components/subscriptions/mne.subscription.ts rename to react-ui/src/stores/reducer/device.reducer/mne.subscription.ts index b2b036007846f030f58c27763516aaee7e8c9ef4..d84e7c5a5a4c954e26e44599df2d2715d9e48590 100644 --- a/react-ui/src/components/subscriptions/mne.subscription.ts +++ b/react-ui/src/stores/reducer/device.reducer/mne.subscription.ts @@ -1,17 +1,17 @@ import { api, NetworkElementServiceGetApiArg } from "@api/api"; -import { Device, setSelectedDevice, setSelectedMne } from "@reducer/device.reducer"; -import { CATEGORIES, triggerSubscription } from "@reducer/subscription.reducer"; +import { Device, setSelectedDevice, setSelectedMne } from "@reducer/device.reducer/device.reducer"; +import { addRoutine, CATEGORIES } from "@reducer/routine.reducer"; import { createAsyncThunk } from "@reduxjs/toolkit"; +import { THUNK_KEY } from "@utils/routine.manager"; import { RootState } from "src/stores"; -import { THUNK_TYPE } from "."; -import { startListening } from "../../../src/stores/middleware/listener.middleware"; -import { FETCH_MNE_ACTION } from "./action.subscription"; +import { FETCH_MNE_ACTION } from "../../../components/routines/action.routine"; +import { startListening } from "../../middleware/listener.middleware"; // fetch mne if selected device is set startListening({ predicate: (action) => setSelectedDevice.match(action) && !!action.payload, effect: async (action, listenerApi) => { - listenerApi.dispatch(triggerSubscription({category: CATEGORIES.TAB, thunkType: THUNK_TYPE.MNE, payload: action.payload})); + listenerApi.dispatch(addRoutine({category: CATEGORIES.TAB, thunkKey: THUNK_KEY.MNE, payload: action.payload})); }, }) diff --git a/react-ui/src/stores/reducer/subscription.reducer.ts b/react-ui/src/stores/reducer/routine.reducer.ts similarity index 51% rename from react-ui/src/stores/reducer/subscription.reducer.ts rename to react-ui/src/stores/reducer/routine.reducer.ts index 344b42d2502bb6bcfc984f242f33c60e7763c5dd..85ddd7b2b11a2fc35d5a3d49b38cdc5b3b540574 100644 --- a/react-ui/src/stores/reducer/subscription.reducer.ts +++ b/react-ui/src/stores/reducer/routine.reducer.ts @@ -1,14 +1,14 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { SubscriptionThunks, THUNK_TYPE } from '@subscription/index'; +import { RoutineDictionary, RoutineManager, THUNK_KEY } from '@utils/routine.manager'; +import { REHYDRATE } from 'redux-persist'; import { RootState } from '..'; -import { SubscriptionHandler } from '../../utils/api/subscription.handler'; import { startListening } from '../middleware/listener.middleware'; import { setToken } from './user.reducer'; interface ThunkEntityDTO { - thunkType: THUNK_TYPE, + thunkKey: THUNK_KEY, payload: any /** @@ -19,11 +19,11 @@ interface ThunkEntityDTO { interface ThunkEntity extends ThunkEntityDTO { id?: number, - locked: boolean + locked: boolean, } -export interface SubscriptionReducerState { +export interface ReducerState { thunks: {[key in keyof typeof CATEGORIES]: ThunkEntity | null} } @@ -32,18 +32,18 @@ export enum CATEGORIES { TAB } -const initialState: SubscriptionReducerState = { +const initialState: ReducerState = { thunks: { TABLE: null, TAB: null } } -const SubscriptionSlice = createSlice({ - name: 'subscription', +const RoutineSlice = createSlice({ + name: 'routine', initialState, reducers: { - triggerSubscription: (state, {payload}: PayloadAction<ThunkEntityDTO>) => { + addRoutine: (state, {payload}: PayloadAction<ThunkEntityDTO>) => { const newThunk: ThunkEntity = {...payload, locked: true}; state.thunks[CATEGORIES[payload.category]] = newThunk; }, @@ -60,51 +60,67 @@ const SubscriptionSlice = createSlice({ }, removeAll: (state) => { - SubscriptionHandler.unsubscribeAll() + RoutineManager.unsubscribeAll() state.thunks = initialState.thunks; }, }, }) -export const { triggerSubscription } = SubscriptionSlice.actions +export const { addRoutine } = RoutineSlice.actions -// on logout remove all subscriptions +// on logout remove all routine startListening({ predicate: (action) => setToken.match(action) && action.payload.token === null, effect: async (_, listenerApi) => { - listenerApi.dispatch(SubscriptionSlice.actions.removeAll()); + listenerApi.dispatch(RoutineSlice.actions.removeAll()); }, }) +// on rehydrate add all persistet routines +startListening({ + predicate: ({type}) => type === REHYDRATE, + effect: async (_, listenerApi) => { + const {routine} = listenerApi.getState() as RootState; + for (const [_,thunk] of Object.entries<ThunkEntity>(routine.thunks)) { + if (!thunk) { + return; + } + + const dto: ThunkEntityDTO = thunk; + listenerApi.dispatch(addRoutine(dto)); + } + }, +}) -// unsubscribe old subscription +// unsubscribe old routine startListening({ - predicate: (action) => triggerSubscription.match(action), + predicate: (action) => addRoutine.match(action), effect: async (action, listenerApi) => { - const {subscription} = listenerApi.getOriginalState() as RootState; - const lastThunk = subscription.thunks[CATEGORIES[action.payload.category]]; - SubscriptionHandler.unsubscribe(lastThunk.id); + const {routine} = listenerApi.getOriginalState() as RootState; + const lastThunk = routine.thunks[CATEGORIES[action.payload.category]]; + if (lastThunk) { + RoutineManager.unsubscribe(lastThunk.id); + } }, }) -// add new subscription +// add new routine startListening({ - predicate: (action) => triggerSubscription.match(action), + predicate: (action) => addRoutine.match(action), effect: async (action, listenerApi) => { - const {thunkType} = action.payload as ThunkEntity; + const {thunkKey} = action.payload as ThunkEntity; - const {thunkFn} = SubscriptionThunks.find(({type}) => type === thunkType); + const thunkFn = RoutineDictionary.get(thunkKey); if (!thunkFn) { // TODO throw new Error('Thunk not found'); } const subscription = await listenerApi.dispatch(thunkFn(action.payload.payload)); - const thunkId = await SubscriptionHandler.add(subscription.payload); - listenerApi.dispatch(SubscriptionSlice.actions.setThunkId({id: thunkId, category: action.payload.category})); - + const thunkId = await RoutineManager.add(subscription.payload); + listenerApi.dispatch(RoutineSlice.actions.setThunkId({id: thunkId, category: action.payload.category})); }, }) -export default SubscriptionSlice.reducer +export default RoutineSlice.reducer diff --git a/react-ui/src/utils/api/subscription.handler.ts b/react-ui/src/utils/api/subscription.handler.ts deleted file mode 100644 index e065879f03d26cd23c4b64ab4c4e2afe749f89b7..0000000000000000000000000000000000000000 --- a/react-ui/src/utils/api/subscription.handler.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { QueryActionCreatorResult } from '@reduxjs/toolkit/query'; - -type Subscription = QueryActionCreatorResult<any>; - -interface Entity { - subscription: Subscription, - id: number -} - - -const initialState = { - subscriptions: [] as Entity[] -} - -export const SubscriptionHandler = (() => { - let state = initialState; - const add = (subscription: Subscription): number => { - const id = state.subscriptions.length; - - const subscriptionEntity: Entity = { - subscription, - id - } - - state.subscriptions = [...state.subscriptions, subscriptionEntity]; - - return id; - } - - - const unsubscribeAll = () => { - state.subscriptions.forEach(({ subscription }) => { - unsubscribeAction(subscription) - }); - - state.subscriptions = initialState.subscriptions; - } - - /** - * @param id - * @returns returns true if the subscription was stopped, false if it was not found - */ - const unsubscribe = (id: number): boolean => { - const subscription = state.subscriptions.find(({ id: subscriptionId }) => subscriptionId === id); - - if (subscription) { - unsubscribeAction(subscription.subscription); - } - - return !!subscription; - } - - /** - * Actual unsubscribe action - * - * @param subscription - */ - const unsubscribeAction = (subscription: Subscription) => { - subscription.unsubscribe(); - } - - - return { - add, - unsubscribe, - unsubscribeAll - } -})(); \ No newline at end of file diff --git a/react-ui/tsconfig.json b/react-ui/tsconfig.json index 41c9c8f6684c18bff5f0ac8aa7c808040e92cfbf..6bd0a18b729d68b99f9f632b42a2d8fefa06675c 100644 --- a/react-ui/tsconfig.json +++ b/react-ui/tsconfig.json @@ -23,17 +23,18 @@ "baseUrl": ".", "paths": { "@assets/*": ["assets/*"], - "@api/*": ["src/utils/api/*"], + "@api/*": ["src/shared/api/*"], "@viewmodel/*": ["src/components/view_model/*"], "@view/*": ["src/components/view/*"], "@reducer/*": ["src/stores/reducer/*"], - "@provider/*": ["src/utils/provider/*"], - "@layout/*": ["src/utils/layouts/*"], + "@provider/*": ["src/shared/provider/*"], + "@layout/*": ["src/shared/layouts/*"], "@hooks": ["src/hooks"], "@routes": ["src/routes.tsx"], - "@task/*": ["src/utils/tasks/*"], - "@helper/*": ["src/utils/helper/*"], - "@subscription/*": ["src/components/subscriptions/*"] + "@task/*": ["src/shared/tasks/*"], + "@helper/*": ["src/shared/helper/*"], + "@routine/*": ["src/components/routines/*"], + "@utils/*": ["src/shared/utils/*"] } }, "include": [ diff --git a/react-ui/vite.config.mjs b/react-ui/vite.config.mjs index 96be1c8444581aa8a9268841dcc275755fb31b45..d36e8cf3a4fddc50ea26a2e19c97a45a88e56961 100644 --- a/react-ui/vite.config.mjs +++ b/react-ui/vite.config.mjs @@ -36,17 +36,18 @@ export default defineConfig({ resolve: { alias: { '@assets': '/assets', - '@api': '/src/utils/api', + '@api': '/src/shared/api', '@viewmodel': '/src/components/view_model', '@view': '/src/components/view', '@reducer': '/src/stores/reducer', - '@provider': '/src/utils/provider', - '@layout': '/src/utils/layouts', + '@provider': '/src/shared/provider', + '@layout': '/src/shared/layouts', '@hooks': '/src/hooks.ts', - '@task': '/src/utils/tasks', - '@helper': '/src/utils/helper', + '@task': '/src/shared/tasks', + '@helper': '/src/shared/helper', '@routes': '/src/routes.tsx', - '@subscription': '/src/components/subscriptions', + '@routine': '/src/components/routines', + '@utils': '/src/shared/utils', }, },