Skip to content

Commit

Permalink
rework and impl. actions queue
Browse files Browse the repository at this point in the history
  • Loading branch information
GreenWizard2015 committed Feb 9, 2024
1 parent dd13daa commit 71b8b59
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 139 deletions.
54 changes: 0 additions & 54 deletions ui/src/components/WaterPumpStatusProvider.js

This file was deleted.

109 changes: 77 additions & 32 deletions ui/src/contexts/WaterPumpAPIContext.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,70 @@
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();

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);
Expand All @@ -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 (
<WaterPumpAPIContext.Provider value={value}>
<WaterPumpStatusProvider>
{children}
</WaterPumpStatusProvider>
{children}
</WaterPumpAPIContext.Provider>
);
}
Expand All @@ -67,7 +112,7 @@ const WaterPumpAPIProvider = connect(
pouringTime: state.UI.pouringTime,
powerLevel: state.UI.powerLevelInPercents,
}),
{ startPump, stopPump }
{ updateStatus: updateSystemStatus }
)(WaterPumpAPIProviderComponent);

export default WaterPumpAPIProvider;
Expand Down
64 changes: 11 additions & 53 deletions ui/src/store/slices/SystemStatus.js
Original file line number Diff line number Diff line change
@@ -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);
}
});

Expand Down

0 comments on commit 71b8b59

Please sign in to comment.