u
This commit is contained in:
100
src/automats/healthAutomat.js
Normal file
100
src/automats/healthAutomat.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @file healthAutomat.js
|
||||
* @description Backend latency samples for the settings "Gesundheit" tab
|
||||
* (`GET /api/health/latency`). Not company-scoped; only polls while a view
|
||||
* has called `actions.startPolling()`.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { health as healthApi } from '../api';
|
||||
|
||||
const POLL_MS = 60_000;
|
||||
|
||||
/**
|
||||
* @typedef {{ timestamp: number, value: number }} LatencyPoint
|
||||
* @typedef {Object} HealthState
|
||||
* @property {'idle'|'loading'|'ready'|'failed'} status
|
||||
* @property {string|null} error
|
||||
* @property {'3h'|'all'} range
|
||||
* @property {LatencyPoint[]} raw Per-minute samples (last 3h)
|
||||
* @property {LatencyPoint[]} avg 30-min rolling 4h average (range=all only)
|
||||
* @property {LatencyPoint[]} max 30-min rolling 4h max (range=all only)
|
||||
*/
|
||||
|
||||
/** @type {HealthState} */
|
||||
const initialState = {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
range: '3h',
|
||||
raw: [],
|
||||
avg: [],
|
||||
max: [],
|
||||
};
|
||||
|
||||
let healthAutomat;
|
||||
let inflight = null;
|
||||
let timer = null;
|
||||
let pollers = 0;
|
||||
|
||||
const actions = {
|
||||
// #region load
|
||||
// MARK: calls GET /api/health/latency from ./api/index.js
|
||||
/**
|
||||
* Loads latency data for the current range. Deduplicates concurrent calls.
|
||||
* @param {boolean} [silent=false] Keep status 'ready' while refreshing
|
||||
*/
|
||||
load(silent = false) {
|
||||
if (inflight) return inflight;
|
||||
const { range, status } = healthAutomat.state;
|
||||
if (!silent && status !== 'ready') healthAutomat.setState({ status: 'loading', error: null });
|
||||
inflight = (async () => {
|
||||
try {
|
||||
const data = await healthApi.getLatency(range);
|
||||
// Range may have changed while the request was in flight.
|
||||
if (healthAutomat.state.range !== range) return;
|
||||
healthAutomat.setState({
|
||||
status: 'ready',
|
||||
error: null,
|
||||
raw: data?.raw ?? [],
|
||||
avg: data?.avg ?? [],
|
||||
max: data?.max ?? [],
|
||||
});
|
||||
} catch (err) {
|
||||
healthAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
||||
} finally {
|
||||
inflight = null;
|
||||
}
|
||||
})();
|
||||
return inflight;
|
||||
},
|
||||
// #endregion
|
||||
|
||||
/** Switches between the live 3h view and the long-term view. */
|
||||
setRange(range) {
|
||||
if (range === healthAutomat.state.range) return;
|
||||
healthAutomat.setState({ range, status: 'loading', error: null });
|
||||
void healthAutomat.actions.load();
|
||||
},
|
||||
|
||||
// #region polling
|
||||
/** Starts the once-a-minute refresh (ref-counted). */
|
||||
startPolling() {
|
||||
pollers += 1;
|
||||
if (pollers > 1) return;
|
||||
void actions.load(healthAutomat.state.status === 'ready');
|
||||
timer = setInterval(() => void actions.load(true), POLL_MS);
|
||||
},
|
||||
|
||||
stopPolling() {
|
||||
pollers = Math.max(0, pollers - 1);
|
||||
if (pollers === 0 && timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
},
|
||||
// #endregion
|
||||
};
|
||||
|
||||
healthAutomat = new Automat(initialState, actions, { name: 'health' });
|
||||
|
||||
export default healthAutomat;
|
||||
Reference in New Issue
Block a user