import { useEffect, useRef } from 'react'; /** * A change writes itself. * * ── Why there is no Save button ── * * There was one, in the panel's bar, announced by whichever section was open — see the git history for * `SaveTarget`. Every screen here is a preview of the thing being edited, so the change is already * visible before it is written; a button then asks somebody to confirm what they can see. What it * really did was make "did that take?" a question, and answer it only after a press. * * ── The debounce is the whole mechanism ── * * A select fires per choice and a text field per keystroke. Waiting for a pause turns a burst into one * request without anybody deciding when the burst ended. * * ── `pending` is a STRING, not a flag ── * * It is the unsaved state, serialised — whatever the panel would send. That is what makes this safe to * retry-free: an attempt is remembered by its value, so a save that FAILS does not immediately run * again on the next render. With a boolean it would: `busy` goes false, `dirty` is still true, and the * effect starts another timer — a failing request every second for as long as the panel is open. * * Null means there is nothing to write, which is different from an empty string. */ const DELAY = 600; export const useAutosave = (pending: string | null, busy: boolean, save: () => void | Promise): void => { // Held in refs so an ordinary function rebuilt every render does not restart the timer. const saveRef = useRef(save); saveRef.current = save; const pendingRef = useRef(pending); pendingRef.current = pending; /** The last value handed to `save`. Compared, so one value is attempted once. */ const tried = useRef(null); useEffect(() => { if (pending === null || busy || tried.current === pending) return; const timer = setTimeout(() => { tried.current = pending; void saveRef.current(); }, DELAY); return () => clearTimeout(timer); }, [pending, busy]); /* Leaving with something unwritten. The panel unmounts when the section changes, and a debounce that has not fired yet would go with it — so a choice made and then navigated away from within half a second would be lost, silently and only sometimes. The request outlives the component; the state updates it makes are no-ops, which is fine because there is nothing left to show them. */ useEffect( () => () => { if (pendingRef.current !== null && pendingRef.current !== tried.current) void saveRef.current(); }, [], ); };