CI/CD
This commit is contained in:
55
ui/src/hooks/useRuntimeState.js
Normal file
55
ui/src/hooks/useRuntimeState.js
Normal 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];
|
||||
}
|
||||
114
ui/src/hooks/useThrottledParameterValue.js
Normal file
114
ui/src/hooks/useThrottledParameterValue.js
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user