Skip to content

Commit 71b8b59

Browse files
rework and impl. actions queue
1 parent dd13daa commit 71b8b59

File tree

3 files changed

+88
-139
lines changed

3 files changed

+88
-139
lines changed

ui/src/components/WaterPumpStatusProvider.js

Lines changed: 0 additions & 54 deletions
This file was deleted.

ui/src/contexts/WaterPumpAPIContext.js

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,70 @@
11
import React from 'react';
22
import { connect } from 'react-redux';
3-
import { startPump, stopPump } from '../store/slices/SystemStatus.js';
43
import { CWaterPumpAPI } from '../api/CWaterPumpAPI.js';
5-
import WaterPumpStatusProvider from '../components/WaterPumpStatusProvider.js';
4+
import { updateSystemStatus } from '../store/slices/SystemStatus.js';
65

76
const WaterPumpAPIContext = React.createContext();
87

98
export function useWaterPumpAPI() {
109
return React.useContext(WaterPumpAPIContext);
1110
}
1211

12+
const FETCH_STATUS_INTERVAL = 5000;
13+
14+
function _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel }) {
15+
if(null == apiObject) return { API: null };
16+
return {
17+
API: {
18+
stopPump: () => {
19+
apiQueue.push({
20+
action: async () => await apiObject.stop(),
21+
failMessage: 'Failed to stop the pump'
22+
});
23+
},
24+
startPump: () => {
25+
apiQueue.push({
26+
action: async () => await apiObject.start(
27+
_pouringTime.current,
28+
_powerLevel.current
29+
),
30+
failMessage: 'Failed to start the pump'
31+
});
32+
},
33+
}
34+
};
35+
}
36+
37+
function _makeStatusAction(apiObject) {
38+
return {
39+
action: async () => await apiObject.status(),
40+
failMessage: 'Failed to get the pump status'
41+
};
42+
}
43+
44+
async function _processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus }) {
45+
const deltaTime = Date.now() - lastUpdateTime.current;
46+
const hasTasks = (0 < apiQueue.length);
47+
if((deltaTime < FETCH_STATUS_INTERVAL) && !hasTasks) return;
48+
49+
const action = hasTasks ? apiQueue.shift() : statusAction;
50+
const oldTime = lastUpdateTime.current;
51+
lastUpdateTime.current = Number.MAX_SAFE_INTEGER; // prevent concurrent tasks, just in case
52+
try {
53+
await updateStatus(action);
54+
lastUpdateTime.current = Date.now();
55+
} catch(error) {
56+
lastUpdateTime.current = oldTime;
57+
if(hasTasks) { // re-queue the action if it failed
58+
apiQueue.unshift(action);
59+
}
60+
throw error;
61+
}
62+
}
63+
1364
function WaterPumpAPIProviderComponent({
1465
children,
1566
apiHost, pouringTime, powerLevel,
16-
startPump, stopPump,
67+
updateStatus,
1768
}) {
1869
// to prevent the callbacks from changing when the pouringTime or powerLevel changes
1970
const _pouringTime = React.useRef(pouringTime);
@@ -22,41 +73,35 @@ function WaterPumpAPIProviderComponent({
2273
const _powerLevel = React.useRef(powerLevel);
2374
React.useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);
2475

25-
const apiObject = React.useMemo(
26-
() => new CWaterPumpAPI({ URL: apiHost }),
76+
const { apiObject, apiQueue } = React.useMemo(
77+
() => ({
78+
apiObject: new CWaterPumpAPI({ URL: apiHost }),
79+
apiQueue: []
80+
}),
2781
[apiHost]
2882
);
2983
////////////////
30-
// create an API wrapper that dispatches actions to the Redux store
31-
const value = React.useMemo(
32-
() => {
33-
if(null == apiObject) return { API: null };
34-
return {
35-
API: {
36-
stopPump: async () => {
37-
return await stopPump({ api: apiObject });
38-
},
39-
startPump: async () => {
40-
return await startPump({
41-
api: apiObject,
42-
pouringTime: _pouringTime.current,
43-
powerLevel: _powerLevel.current,
44-
});
45-
},
46-
status: async () => {
47-
return await apiObject.status();
48-
}
49-
}
50-
};
51-
},
52-
[apiObject, startPump, stopPump, _pouringTime, _powerLevel]
84+
const statusAction = React.useMemo(() => _makeStatusAction(apiObject), [apiObject]);
85+
const lastUpdateTime = React.useRef(0);
86+
const onTick = React.useCallback(
87+
async () => _processQueue({ apiQueue, lastUpdateTime, statusAction, updateStatus }),
88+
[apiQueue, lastUpdateTime, updateStatus, statusAction]
5389
);
5490

91+
// Run the timer
92+
React.useEffect(() => {
93+
const timer = setInterval(onTick, 100);
94+
return () => { clearInterval(timer); };
95+
}, [onTick]);
96+
97+
////////////////
98+
const value = React.useMemo(
99+
() => _publicWrapper({ apiObject, apiQueue, _pouringTime, _powerLevel }),
100+
[apiObject, apiQueue, _pouringTime, _powerLevel]
101+
);
55102
return (
56103
<WaterPumpAPIContext.Provider value={value}>
57-
<WaterPumpStatusProvider>
58-
{children}
59-
</WaterPumpStatusProvider>
104+
{children}
60105
</WaterPumpAPIContext.Provider>
61106
);
62107
}
@@ -67,7 +112,7 @@ const WaterPumpAPIProvider = connect(
67112
pouringTime: state.UI.pouringTime,
68113
powerLevel: state.UI.powerLevelInPercents,
69114
}),
70-
{ startPump, stopPump }
115+
{ updateStatus: updateSystemStatus }
71116
)(WaterPumpAPIProviderComponent);
72117

73118
export default WaterPumpAPIProvider;

ui/src/store/slices/SystemStatus.js

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,29 @@
11
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
22
import { NotificationsSystemActions } from './Notifications';
33

4-
function withNotification(action, message) {
5-
return async (params, { dispatch }) => {
4+
// Async thunks
5+
export const updateSystemStatus = createAsyncThunk(
6+
'systemStatus/update',
7+
async ({ action, failMessage }, { dispatch }) => {
68
try {
7-
return await action(params);
9+
return await action();
810
} catch(error) {
9-
dispatch(NotificationsSystemActions.alert({
11+
await dispatch(NotificationsSystemActions.alert({
1012
type: 'error',
11-
message: `${message} (${error.message})`
13+
message: `${failMessage} (${error.message})`
1214
}));
1315
throw error;
1416
}
15-
};
16-
}
17-
18-
// Async thunks
19-
export const startPump = createAsyncThunk(
20-
'systemStatus/startPump',
21-
withNotification(
22-
async ({ api, pouringTime, powerLevel }) => {
23-
return await api.start(pouringTime, powerLevel);
24-
},
25-
'Failed to start pump'
26-
)
27-
);
28-
29-
export const stopPump = createAsyncThunk(
30-
'systemStatus/stopPump',
31-
withNotification(
32-
async ({ api }) => {
33-
return await api.stop();
34-
},
35-
'Failed to stop pump'
36-
)
37-
);
38-
39-
export const updateSystemStatus = createAsyncThunk(
40-
'systemStatus/update',
41-
withNotification(
42-
async ({ api }) => {
43-
return await api.status();
44-
},
45-
'Failed to update system status'
46-
)
47-
);
48-
49-
// slice for system status
50-
const bindStatus = (state, action) => {
51-
return action.payload;
52-
};
17+
}
18+
);
5319

5420
export const SystemStatusSlice = createSlice({
5521
name: 'systemStatus',
5622
initialState: null,
5723
reducers: {},
5824
extraReducers: (builder) => {
59-
// update system status on start/stop pump
60-
builder.addCase(startPump.fulfilled, bindStatus);
61-
builder.addCase(stopPump.fulfilled, bindStatus);
62-
builder.addCase(updateSystemStatus.fulfilled, bindStatus);
63-
// on error, do not update system status
64-
builder.addCase(startPump.rejected, (state, action) => state);
65-
builder.addCase(stopPump.rejected, (state, action) => state);
66-
builder.addCase(updateSystemStatus.rejected, (state, action) => {
67-
return null;
68-
});
25+
builder.addCase(updateSystemStatus.fulfilled, (state, action) => action.payload);
26+
builder.addCase(updateSystemStatus.rejected, (state, action) => state);
6927
}
7028
});
7129

0 commit comments

Comments
 (0)