Files
video-shader-toys/ui/src/hooks/useThrottledParameterValue.js
Aiden c39a1fd53c
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 21s
CI / React UI Build (push) Has been cancelled
CI/CD
2026-05-03 11:26:10 +10:00

115 lines
3.0 KiB
JavaScript

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,
};
}