diff --git a/ui/src/components/WaterPumpStatusProvider.js b/ui/src/components/WaterPumpStatusProvider.js
deleted file mode 100644
index 5727fde..0000000
--- a/ui/src/components/WaterPumpStatusProvider.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { updateSystemStatus } from '../store/slices/SystemStatus';
-import { useWaterPumpAPI } from '../contexts/WaterPumpAPIContext';
-
-const FETCH_INTERVAL = 5000;
-const CHECK_INTERVAL = Math.round(FETCH_INTERVAL / 10);
-
-function WaterPumpStatusProviderComoponent({ children, updateStatus, systemStatus }) {
- const { API } = useWaterPumpAPI();
- const nextFetchTime = React.useRef(0);
-
- // Function to fetch water pump status
- const fetchStatus = React.useCallback(async () => {
- const now = Date.now();
- if(now < nextFetchTime.current) return;
- if(null == API) return;
-
- nextFetchTime.current = Number.MAX_SAFE_INTEGER; // prevent concurrent fetches
- await updateStatus({ api: API });
- nextFetchTime.current = Date.now() + FETCH_INTERVAL;
- },
- [API, updateStatus, nextFetchTime]
- );
-
- // Effect to start fetching periodically and when API changes
- React.useEffect(() => {
- const timer = setInterval(fetchStatus, CHECK_INTERVAL);
- return () => { clearInterval(timer); };
- }, [fetchStatus]);
-
- // Effect to reset timer when system status changes
- React.useEffect(() => {
- // reset timer if not fetching
- const now = Date.now();
- if(now < nextFetchTime.current) {
- nextFetchTime.current = 0;
- }
- }, [API, systemStatus, nextFetchTime]);
-
- return (
-
- {children}
-
- );
-}
-
-export default connect(
- (state) => ({
- systemStatus: state.systemStatus
- }), {
- updateStatus: updateSystemStatus
- }
-)(WaterPumpStatusProviderComoponent);
\ No newline at end of file
diff --git a/ui/src/contexts/WaterPumpAPIContext.js b/ui/src/contexts/WaterPumpAPIContext.js
index ce8b453..92ff269 100644
--- a/ui/src/contexts/WaterPumpAPIContext.js
+++ b/ui/src/contexts/WaterPumpAPIContext.js
@@ -1,8 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
-import { startPump, stopPump } from '../store/slices/SystemStatus.js';
import { CWaterPumpAPI } from '../api/CWaterPumpAPI.js';
-import WaterPumpStatusProvider from '../components/WaterPumpStatusProvider.js';
+import { updateSystemStatus } from '../store/slices/SystemStatus.js';
const WaterPumpAPIContext = React.createContext();
@@ -10,10 +9,62 @@ export function useWaterPumpAPI() {
return React.useContext(WaterPumpAPIContext);
}
+const FETCH_STATUS_INTERVAL = 5000;
+
+function _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel }) {
+ if(null == apiObject) return { API: null };
+ return {
+ API: {
+ stopPump: () => {
+ apiQueue.push({
+ action: async () => await apiObject.stop(),
+ failMessage: 'Failed to stop the pump'
+ });
+ },
+ startPump: () => {
+ apiQueue.push({
+ action: async () => await apiObject.start(
+ _pouringTime.current,
+ _powerLevel.current
+ ),
+ failMessage: 'Failed to start the pump'
+ });
+ },
+ }
+ };
+}
+
+function _makeStatusAction(apiObject) {
+ return {
+ action: async () => await apiObject.status(),
+ failMessage: 'Failed to get the pump status'
+ };
+}
+
+async function _processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus }) {
+ const deltaTime = Date.now() - lastUpdateTime.current;
+ const hasTasks = (0 < apiQueue.length);
+ if((deltaTime < FETCH_STATUS_INTERVAL) && !hasTasks) return;
+
+ const action = hasTasks ? apiQueue.shift() : statusAction;
+ const oldTime = lastUpdateTime.current;
+ lastUpdateTime.current = Number.MAX_SAFE_INTEGER; // prevent concurrent tasks, just in case
+ try {
+ await updateStatus(action);
+ lastUpdateTime.current = Date.now();
+ } catch(error) {
+ lastUpdateTime.current = oldTime;
+ if(hasTasks) { // re-queue the action if it failed
+ apiQueue.unshift(action);
+ }
+ throw error;
+ }
+}
+
function WaterPumpAPIProviderComponent({
children,
apiHost, pouringTime, powerLevel,
- startPump, stopPump,
+ updateStatus,
}) {
// to prevent the callbacks from changing when the pouringTime or powerLevel changes
const _pouringTime = React.useRef(pouringTime);
@@ -22,41 +73,35 @@ function WaterPumpAPIProviderComponent({
const _powerLevel = React.useRef(powerLevel);
React.useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);
- const apiObject = React.useMemo(
- () => new CWaterPumpAPI({ URL: apiHost }),
+ const { apiObject, apiQueue } = React.useMemo(
+ () => ({
+ apiObject: new CWaterPumpAPI({ URL: apiHost }),
+ apiQueue: []
+ }),
[apiHost]
);
////////////////
- // create an API wrapper that dispatches actions to the Redux store
- const value = React.useMemo(
- () => {
- if(null == apiObject) return { API: null };
- return {
- API: {
- stopPump: async () => {
- return await stopPump({ api: apiObject });
- },
- startPump: async () => {
- return await startPump({
- api: apiObject,
- pouringTime: _pouringTime.current,
- powerLevel: _powerLevel.current,
- });
- },
- status: async () => {
- return await apiObject.status();
- }
- }
- };
- },
- [apiObject, startPump, stopPump, _pouringTime, _powerLevel]
+ const statusAction = React.useMemo(() => _makeStatusAction(apiObject), [apiObject]);
+ const lastUpdateTime = React.useRef(0);
+ const onTick = React.useCallback(
+ async () => _processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus }),
+ [apiQueue, lastUpdateTime, updateStatus, statusAction]
);
+ // Run the timer
+ React.useEffect(() => {
+ const timer = setInterval(onTick, 100);
+ return () => { clearInterval(timer); };
+ }, [onTick]);
+
+ ////////////////
+ const value = React.useMemo(
+ () => _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel }),
+ [apiObject, apiQueue, _pouringTime, _powerLevel]
+ );
return (
-
- {children}
-
+ {children}
);
}
@@ -67,7 +112,7 @@ const WaterPumpAPIProvider = connect(
pouringTime: state.UI.pouringTime,
powerLevel: state.UI.powerLevelInPercents,
}),
- { startPump, stopPump }
+ { updateStatus: updateSystemStatus }
)(WaterPumpAPIProviderComponent);
export default WaterPumpAPIProvider;
diff --git a/ui/src/store/slices/SystemStatus.js b/ui/src/store/slices/SystemStatus.js
index 074479f..c0789ac 100644
--- a/ui/src/store/slices/SystemStatus.js
+++ b/ui/src/store/slices/SystemStatus.js
@@ -1,71 +1,29 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { NotificationsSystemActions } from './Notifications';
-function withNotification(action, message) {
- return async (params, { dispatch }) => {
+// Async thunks
+export const updateSystemStatus = createAsyncThunk(
+ 'systemStatus/update',
+ async ({ action, failMessage }, { dispatch }) => {
try {
- return await action(params);
+ return await action();
} catch(error) {
- dispatch(NotificationsSystemActions.alert({
+ await dispatch(NotificationsSystemActions.alert({
type: 'error',
- message: `${message} (${error.message})`
+ message: `${failMessage} (${error.message})`
}));
throw error;
}
- };
-}
-
-// Async thunks
-export const startPump = createAsyncThunk(
- 'systemStatus/startPump',
- withNotification(
- async ({ api, pouringTime, powerLevel }) => {
- return await api.start(pouringTime, powerLevel);
- },
- 'Failed to start pump'
- )
-);
-
-export const stopPump = createAsyncThunk(
- 'systemStatus/stopPump',
- withNotification(
- async ({ api }) => {
- return await api.stop();
- },
- 'Failed to stop pump'
- )
-);
-
-export const updateSystemStatus = createAsyncThunk(
- 'systemStatus/update',
- withNotification(
- async ({ api }) => {
- return await api.status();
- },
- 'Failed to update system status'
- )
-);
-
-// slice for system status
-const bindStatus = (state, action) => {
- return action.payload;
-};
+ }
+);
export const SystemStatusSlice = createSlice({
name: 'systemStatus',
initialState: null,
reducers: {},
extraReducers: (builder) => {
- // update system status on start/stop pump
- builder.addCase(startPump.fulfilled, bindStatus);
- builder.addCase(stopPump.fulfilled, bindStatus);
- builder.addCase(updateSystemStatus.fulfilled, bindStatus);
- // on error, do not update system status
- builder.addCase(startPump.rejected, (state, action) => state);
- builder.addCase(stopPump.rejected, (state, action) => state);
- builder.addCase(updateSystemStatus.rejected, (state, action) => {
- return null;
- });
+ builder.addCase(updateSystemStatus.fulfilled, (state, action) => action.payload);
+ builder.addCase(updateSystemStatus.rejected, (state, action) => state);
}
});