65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
// "A few seconds" per the group session's live-editing requirement --
|
|
// short enough to feel live, long enough not to fire a save on every
|
|
// keystroke. Deliberately well under board-context's 30s cross-device
|
|
// poll interval, so a local edit always settles before the poll could
|
|
// otherwise consider overwriting it.
|
|
export const AUTOSAVE_DELAY_MS = 2500;
|
|
|
|
/**
|
|
* Debounced autosave for a single field's value. Call `onSave` a beat
|
|
* after `value` stops changing, and again immediately via `flush()` (used
|
|
* by a manual "Save" button and by flush-on-close) -- both paths funnel
|
|
* through the same `runSave`, so there's no separate "save now" logic to
|
|
* keep in sync with the debounced one.
|
|
*
|
|
* `onSave` must be one of the board-context mutators (e.g. `saveNote`,
|
|
* `editTodo`), never a raw server action directly -- those mutators already
|
|
* mark the write as in-flight (see `track()` in board-context.tsx) so the
|
|
* app's cross-device poll doesn't clobber it mid-save.
|
|
*/
|
|
export function useAutosave<T>(
|
|
value: T,
|
|
onSave: (value: T) => Promise<boolean | void>,
|
|
delayMs = AUTOSAVE_DELAY_MS
|
|
): { flush: () => Promise<void>; isDirty: boolean; isSaving: boolean } {
|
|
const [savedValue, setSavedValue] = useState(value);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const onSaveRef = useRef(onSave);
|
|
const savingPromiseRef = useRef<Promise<void> | null>(null);
|
|
|
|
useEffect(() => {
|
|
onSaveRef.current = onSave;
|
|
}, [onSave]);
|
|
|
|
const runSave = useCallback((v: T) => {
|
|
setIsSaving(true);
|
|
const promise = (async () => {
|
|
await onSaveRef.current(v);
|
|
setSavedValue(v);
|
|
setIsSaving(false);
|
|
})();
|
|
savingPromiseRef.current = promise;
|
|
return promise;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (value === savedValue) return;
|
|
const timer = setTimeout(() => {
|
|
void runSave(value);
|
|
}, delayMs);
|
|
return () => clearTimeout(timer);
|
|
}, [value, savedValue, delayMs, runSave]);
|
|
|
|
const flush = useCallback(async () => {
|
|
if (value !== savedValue) {
|
|
await runSave(value);
|
|
} else if (savingPromiseRef.current) {
|
|
await savingPromiseRef.current;
|
|
}
|
|
}, [value, savedValue, runSave]);
|
|
|
|
return { flush, isDirty: value !== savedValue, isSaving };
|
|
}
|