Skip to content

Commit e9c38ef

Browse files
Merge pull request psmgeelen#13 from GreenWizard2015/Handling-Rapid-Sequential-Requests
Handling rapid sequential requests
2 parents 34c73e1 + 9ccad0e commit e9c38ef

File tree

6 files changed

+139
-171
lines changed

6 files changed

+139
-171
lines changed

ui/src/components/HoldToPour.js

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react';
22
import { connect } from 'react-redux';
33
import { Container, Form } from 'react-bootstrap';
44
import { useWaterPumpAPI } from '../contexts/WaterPumpAPIContext';
5-
import { startPump, stopPump } from '../store/slices/SystemStatus.js';
65

7-
export function HoldToPourComponent({ startPump, stopPump, interval }) {
6+
export function HoldToPourComponent({ interval }) {
7+
const { API }= useWaterPumpAPI();
88
const [isPouring, setIsPouring] = useState(false);
99
const [clickToPour, setClickToPour] = useState(false);
1010
// continuously pour water while the button is pressed
@@ -14,28 +14,28 @@ export function HoldToPourComponent({ startPump, stopPump, interval }) {
1414
if(Date.now() < lastPouringTime.current) return;
1515
try {
1616
lastPouringTime.current = Number.MAX_SAFE_INTEGER; // prevent concurrent calls
17-
await startPump();
17+
await API.startPump();
1818
lastPouringTime.current = Date.now() + interval;
1919
} catch(e) {
2020
lastPouringTime.current = 0; // run again on next tick
2121
}
2222
},
23-
[startPump, interval]
23+
[interval, API]
2424
);
2525

2626
useEffect(() => {
2727
if(!isPouring) {
2828
lastPouringTime.current = 0;
29-
stopPump();
29+
API.stopPump();
3030
return;
3131
}
3232
// tick every 100ms
3333
const tid = setInterval(onTick, 100);
3434
return async () => {
3535
clearInterval(tid);
36-
if(isPouring) await stopPump();
36+
if(isPouring) await API.stopPump();
3737
};
38-
}, [onTick, isPouring, stopPump, lastPouringTime]);
38+
}, [onTick, isPouring, API]);
3939

4040
const handlePress = () => { setIsPouring(true); };
4141
const handleRelease = () => { setIsPouring(false); };
@@ -65,43 +65,18 @@ export function HoldToPourComponent({ startPump, stopPump, interval }) {
6565
}
6666

6767
// Helper wrapper to simplify the code in the component
68-
function HoldToPourComponent_withExtras({ pouringTime, powerLevel, startPump, stopPump }) {
69-
const api = useWaterPumpAPI().API;
70-
// to prevent the callback from changing when the pouringTime or powerLevel changes
71-
const _pouringTime = React.useRef(pouringTime);
72-
React.useEffect(() => { _pouringTime.current = pouringTime; }, [pouringTime]);
73-
74-
const _powerLevel = React.useRef(powerLevel);
75-
React.useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);
76-
77-
const _startPump = React.useCallback(
78-
async () => {
79-
await startPump({
80-
api,
81-
pouringTime: _pouringTime.current,
82-
powerLevel: _powerLevel.current,
83-
});
84-
}, [api, startPump, _pouringTime, _powerLevel]
85-
);
86-
const _stopPump = React.useCallback(
87-
async () => { await stopPump({ api }); },
88-
[api, stopPump]
89-
);
68+
function HoldToPourComponent_withExtras({ pouringTime, ...props }) {
9069
// a bit smaller than the actual pouring time, to prevent the pump from stopping
9170
// which could damage the pump
9271
const interval = Math.max(Math.round(pouringTime - 500), 100);
9372
return (
94-
<HoldToPourComponent
95-
startPump={_startPump} stopPump={_stopPump}
96-
interval={interval}
97-
/>
73+
<HoldToPourComponent {...props} interval={interval} />
9874
);
9975
};
10076

10177
export default connect(
10278
state => ({
10379
pouringTime: state.UI.pouringTime,
104-
powerLevel: state.UI.powerLevelInPercents,
10580
}),
106-
{ startPump, stopPump }
81+
{ }
10782
)(HoldToPourComponent_withExtras);

ui/src/components/SystemControls.js

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,11 @@ import { connect } from 'react-redux';
33
import { Button, Container } from 'react-bootstrap';
44

55
import { useWaterPumpAPI } from '../contexts/WaterPumpAPIContext';
6-
import { startPump, stopPump } from '../store/slices/SystemStatus.js';
76

8-
export function SystemControlsComponent({
9-
pouringTime, powerLevel, systemStatus, startPump, stopPump
10-
}) {
11-
const api = useWaterPumpAPI().API;
12-
const handleStart = async () => {
13-
await startPump({ api, pouringTime, powerLevel });
14-
};
15-
16-
const handleStop = async () => {
17-
await stopPump({ api });
18-
};
7+
export function SystemControlsComponent({ systemStatus }) {
8+
const { API } = useWaterPumpAPI();
9+
const handleStart = async () => { await API.startPump(); };
10+
const handleStop = async () => { await API.stopPump(); };
1911

2012
const isRunning = systemStatus.pump.running;
2113
return (
@@ -32,8 +24,6 @@ export function SystemControlsComponent({
3224

3325
export default connect(
3426
state => ({
35-
pouringTime: state.UI.pouringTime,
36-
powerLevel: state.UI.powerLevelInPercents,
3727
systemStatus: state.systemStatus,
38-
}), { startPump, stopPump }
28+
}), { }
3929
)(SystemControlsComponent);

ui/src/components/WaterPumpStatusProvider.js

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,119 @@
11
import React from 'react';
2-
import { useSelector } from 'react-redux';
2+
import { connect } from 'react-redux';
33
import { CWaterPumpAPI } from '../api/CWaterPumpAPI.js';
4-
import WaterPumpStatusProvider from '../components/WaterPumpStatusProvider.js';
4+
import { updateSystemStatus } from '../store/slices/SystemStatus.js';
55

66
const WaterPumpAPIContext = React.createContext();
77

88
export function useWaterPumpAPI() {
99
return React.useContext(WaterPumpAPIContext);
1010
}
1111

12-
export function WaterPumpAPIProvider({ children }) {
13-
const apiHost = useSelector((state) => state.UI.apiHost);
14-
const apiObject = React.useMemo(
15-
() => new CWaterPumpAPI({ URL: apiHost }),
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+
64+
function WaterPumpAPIProviderComponent({
65+
children,
66+
apiHost, pouringTime, powerLevel,
67+
updateStatus,
68+
}) {
69+
// to prevent the callbacks from changing when the pouringTime or powerLevel changes
70+
const _pouringTime = React.useRef(pouringTime);
71+
React.useEffect(() => { _pouringTime.current = pouringTime; }, [pouringTime]);
72+
73+
const _powerLevel = React.useRef(powerLevel);
74+
React.useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);
75+
76+
const { apiObject, apiQueue } = React.useMemo(
77+
() => ({
78+
apiObject: new CWaterPumpAPI({ URL: apiHost }),
79+
apiQueue: []
80+
}),
1681
[apiHost]
1782
);
18-
// TODO: provide also the API methods with binded values from the store
19-
// to simplify the code in the components (HodlToPour and PowerLevel)
20-
const value = { API: apiObject, };
83+
////////////////
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]
89+
);
90+
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+
);
21102
return (
22103
<WaterPumpAPIContext.Provider value={value}>
23-
<WaterPumpStatusProvider>
24-
{children}
25-
</WaterPumpStatusProvider>
104+
{children}
26105
</WaterPumpAPIContext.Provider>
27106
);
28-
}
107+
}
108+
109+
const WaterPumpAPIProvider = connect(
110+
state => ({
111+
apiHost: state.UI.apiHost,
112+
pouringTime: state.UI.pouringTime,
113+
powerLevel: state.UI.powerLevelInPercents,
114+
}),
115+
{ updateStatus: updateSystemStatus }
116+
)(WaterPumpAPIProviderComponent);
117+
118+
export default WaterPumpAPIProvider;
119+
export { WaterPumpAPIProvider };

ui/src/store/index.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { combineReducers, configureStore } from "@reduxjs/toolkit";
22
import { Provider } from "react-redux";
3-
import { persistStore, persistReducer } from 'redux-persist';
3+
import {
4+
persistReducer, persistStore,
5+
FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER
6+
} from "redux-persist";
47
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
58

69
// slices
@@ -50,6 +53,11 @@ const AppStore = ({ children, preloadedState = {}, returnStore = false }) => {
5053
const store = configureStore({
5154
reducer: persistedReducer,
5255
preloadedState: state,
56+
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
57+
serializableCheck: {
58+
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
59+
}
60+
})
5361
});
5462
const persistor = persistStore(store);
5563

0 commit comments

Comments
 (0)