Skip to content

Commit

Permalink
Merge pull request #13 from GreenWizard2015/Handling-Rapid-Sequential…
Browse files Browse the repository at this point in the history
…-Requests

Handling rapid sequential requests
  • Loading branch information
GreenWizard2015 authored Feb 11, 2024
2 parents 34c73e1 + 9ccad0e commit e9c38ef
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 171 deletions.
45 changes: 10 additions & 35 deletions ui/src/components/HoldToPour.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { Container, Form } from 'react-bootstrap';
import { useWaterPumpAPI } from '../contexts/WaterPumpAPIContext';
import { startPump, stopPump } from '../store/slices/SystemStatus.js';

export function HoldToPourComponent({ startPump, stopPump, interval }) {
export function HoldToPourComponent({ interval }) {
const { API }= useWaterPumpAPI();
const [isPouring, setIsPouring] = useState(false);
const [clickToPour, setClickToPour] = useState(false);
// continuously pour water while the button is pressed
Expand All @@ -14,28 +14,28 @@ export function HoldToPourComponent({ startPump, stopPump, interval }) {
if(Date.now() < lastPouringTime.current) return;
try {
lastPouringTime.current = Number.MAX_SAFE_INTEGER; // prevent concurrent calls
await startPump();
await API.startPump();
lastPouringTime.current = Date.now() + interval;
} catch(e) {
lastPouringTime.current = 0; // run again on next tick
}
},
[startPump, interval]
[interval, API]
);

useEffect(() => {
if(!isPouring) {
lastPouringTime.current = 0;
stopPump();
API.stopPump();
return;
}
// tick every 100ms
const tid = setInterval(onTick, 100);
return async () => {
clearInterval(tid);
if(isPouring) await stopPump();
if(isPouring) await API.stopPump();
};
}, [onTick, isPouring, stopPump, lastPouringTime]);
}, [onTick, isPouring, API]);

const handlePress = () => { setIsPouring(true); };
const handleRelease = () => { setIsPouring(false); };
Expand Down Expand Up @@ -65,43 +65,18 @@ export function HoldToPourComponent({ startPump, stopPump, interval }) {
}

// Helper wrapper to simplify the code in the component
function HoldToPourComponent_withExtras({ pouringTime, powerLevel, startPump, stopPump }) {
const api = useWaterPumpAPI().API;
// to prevent the callback from changing when the pouringTime or powerLevel changes
const _pouringTime = React.useRef(pouringTime);
React.useEffect(() => { _pouringTime.current = pouringTime; }, [pouringTime]);

const _powerLevel = React.useRef(powerLevel);
React.useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);

const _startPump = React.useCallback(
async () => {
await startPump({
api,
pouringTime: _pouringTime.current,
powerLevel: _powerLevel.current,
});
}, [api, startPump, _pouringTime, _powerLevel]
);
const _stopPump = React.useCallback(
async () => { await stopPump({ api }); },
[api, stopPump]
);
function HoldToPourComponent_withExtras({ pouringTime, ...props }) {
// a bit smaller than the actual pouring time, to prevent the pump from stopping
// which could damage the pump
const interval = Math.max(Math.round(pouringTime - 500), 100);
return (
<HoldToPourComponent
startPump={_startPump} stopPump={_stopPump}
interval={interval}
/>
<HoldToPourComponent {...props} interval={interval} />
);
};

export default connect(
state => ({
pouringTime: state.UI.pouringTime,
powerLevel: state.UI.powerLevelInPercents,
}),
{ startPump, stopPump }
{ }
)(HoldToPourComponent_withExtras);
20 changes: 5 additions & 15 deletions ui/src/components/SystemControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,11 @@ import { connect } from 'react-redux';
import { Button, Container } from 'react-bootstrap';

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

export function SystemControlsComponent({
pouringTime, powerLevel, systemStatus, startPump, stopPump
}) {
const api = useWaterPumpAPI().API;
const handleStart = async () => {
await startPump({ api, pouringTime, powerLevel });
};

const handleStop = async () => {
await stopPump({ api });
};
export function SystemControlsComponent({ systemStatus }) {
const { API } = useWaterPumpAPI();
const handleStart = async () => { await API.startPump(); };
const handleStop = async () => { await API.stopPump(); };

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

export default connect(
state => ({
pouringTime: state.UI.pouringTime,
powerLevel: state.UI.powerLevelInPercents,
systemStatus: state.systemStatus,
}), { startPump, stopPump }
}), { }
)(SystemControlsComponent);
54 changes: 0 additions & 54 deletions ui/src/components/WaterPumpStatusProvider.js

This file was deleted.

117 changes: 104 additions & 13 deletions ui/src/contexts/WaterPumpAPIContext.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,119 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { connect } from 'react-redux';
import { CWaterPumpAPI } from '../api/CWaterPumpAPI.js';
import WaterPumpStatusProvider from '../components/WaterPumpStatusProvider.js';
import { updateSystemStatus } from '../store/slices/SystemStatus.js';

const WaterPumpAPIContext = React.createContext();

export function useWaterPumpAPI() {
return React.useContext(WaterPumpAPIContext);
}

export function WaterPumpAPIProvider({ children }) {
const apiHost = useSelector((state) => state.UI.apiHost);
const apiObject = React.useMemo(
() => new CWaterPumpAPI({ URL: apiHost }),
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,
updateStatus,
}) {
// to prevent the callbacks from changing when the pouringTime or powerLevel changes
const _pouringTime = React.useRef(pouringTime);
React.useEffect(() => { _pouringTime.current = pouringTime; }, [pouringTime]);

const _powerLevel = React.useRef(powerLevel);
React.useEffect(() => { _powerLevel.current = powerLevel; }, [powerLevel]);

const { apiObject, apiQueue } = React.useMemo(
() => ({
apiObject: new CWaterPumpAPI({ URL: apiHost }),
apiQueue: []
}),
[apiHost]
);
// TODO: provide also the API methods with binded values from the store
// to simplify the code in the components (HodlToPour and PowerLevel)
const value = { API: apiObject, };
////////////////
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 (
<WaterPumpAPIContext.Provider value={value}>
<WaterPumpStatusProvider>
{children}
</WaterPumpStatusProvider>
{children}
</WaterPumpAPIContext.Provider>
);
}
}

const WaterPumpAPIProvider = connect(
state => ({
apiHost: state.UI.apiHost,
pouringTime: state.UI.pouringTime,
powerLevel: state.UI.powerLevelInPercents,
}),
{ updateStatus: updateSystemStatus }
)(WaterPumpAPIProviderComponent);

export default WaterPumpAPIProvider;
export { WaterPumpAPIProvider };
10 changes: 9 additions & 1 deletion ui/src/store/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import { persistStore, persistReducer } from 'redux-persist';
import {
persistReducer, persistStore,
FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER
} from "redux-persist";
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web

// slices
Expand Down Expand Up @@ -50,6 +53,11 @@ const AppStore = ({ children, preloadedState = {}, returnStore = false }) => {
const store = configureStore({
reducer: persistedReducer,
preloadedState: state,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
}
})
});
const persistor = persistStore(store);

Expand Down
Loading

0 comments on commit e9c38ef

Please sign in to comment.