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