CI/CD
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 21s
CI / React UI Build (push) Has been cancelled

This commit is contained in:
2026-05-03 11:26:10 +10:00
parent ee929374a8
commit c39a1fd53c
12 changed files with 848 additions and 701 deletions

View File

@@ -0,0 +1,55 @@
import { useEffect, useState } from "react";
export function useRuntimeState() {
const [appState, setAppState] = useState(null);
useEffect(() => {
let mounted = true;
let socket;
let retryTimer;
const connectWebSocket = () => {
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
socket = new WebSocket(`${protocol}://${window.location.host}/ws`);
socket.onmessage = (event) => {
try {
const nextState = JSON.parse(event.data);
if (mounted) {
setAppState(nextState);
}
} catch (error) {
console.error("Failed to parse state update", error);
}
};
socket.onclose = () => {
if (mounted) {
retryTimer = window.setTimeout(connectWebSocket, 1000);
}
};
};
fetch("/api/state")
.then((response) => response.json())
.then((state) => {
if (mounted) {
setAppState(state);
connectWebSocket();
}
})
.catch((error) => {
console.error("Failed to load initial state", error);
});
return () => {
mounted = false;
if (retryTimer) {
window.clearTimeout(retryTimer);
}
if (socket) {
socket.close();
}
};
}, []);
return [appState, setAppState];
}

View File

@@ -0,0 +1,114 @@
import { useEffect, useRef, useState } from "react";
function valuesMatch(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
export function useThrottledParameterValue(parameter, onParameterChange) {
const [draftValue, setDraftValue] = useState(parameter.value);
const [appliedValue, setAppliedValue] = useState(parameter.value);
const pendingTimeoutRef = useRef(null);
const latestDraftRef = useRef(parameter.value);
const lastSentAtRef = useRef(0);
const isInteractingRef = useRef(false);
const isDirtyRef = useRef(false);
useEffect(() => {
setDraftValue(parameter.value);
setAppliedValue(parameter.value);
latestDraftRef.current = parameter.value;
lastSentAtRef.current = 0;
isInteractingRef.current = false;
isDirtyRef.current = false;
}, [parameter.id]);
useEffect(() => {
setAppliedValue(parameter.value);
latestDraftRef.current = draftValue;
if (isDirtyRef.current && valuesMatch(parameter.value, latestDraftRef.current)) {
isDirtyRef.current = false;
}
if (!isInteractingRef.current && !isDirtyRef.current) {
setDraftValue(parameter.value);
}
}, [draftValue, parameter.value]);
useEffect(() => {
return () => {
if (pendingTimeoutRef.current) {
clearTimeout(pendingTimeoutRef.current);
}
};
}, []);
const sendValue = (value) => {
setDraftValue(value);
latestDraftRef.current = value;
isDirtyRef.current = true;
lastSentAtRef.current = Date.now();
const request = onParameterChange(parameter.id, value);
if (request && typeof request.then === "function") {
request
.then((response) => {
if (response?.ok) {
setAppliedValue(value);
}
})
.catch(() => {
// Keep showing the last confirmed value if the update failed.
});
}
};
const scheduleSendValue = (value, immediate = false) => {
setDraftValue(value);
latestDraftRef.current = value;
isDirtyRef.current = true;
if (pendingTimeoutRef.current) {
clearTimeout(pendingTimeoutRef.current);
pendingTimeoutRef.current = null;
}
const now = Date.now();
const throttleMs = 90;
const elapsed = now - lastSentAtRef.current;
if (immediate || elapsed >= throttleMs) {
sendValue(value);
return;
}
pendingTimeoutRef.current = setTimeout(() => {
pendingTimeoutRef.current = null;
sendValue(latestDraftRef.current);
}, throttleMs - elapsed);
};
const flushScheduledValue = () => {
if (pendingTimeoutRef.current) {
clearTimeout(pendingTimeoutRef.current);
pendingTimeoutRef.current = null;
}
sendValue(latestDraftRef.current);
};
const beginInteraction = () => {
isInteractingRef.current = true;
};
const endInteraction = () => {
isInteractingRef.current = false;
flushScheduledValue();
};
return {
appliedValue,
beginInteraction,
draftValue,
endInteraction,
isPending: !valuesMatch(draftValue, appliedValue),
scheduleSendValue,
sendValue,
};
}