115 lines
3.0 KiB
JavaScript
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,
|
|
};
|
|
}
|