"use client"; import { useCallback, useEffect, useRef, useState } from "react"; export type AutoSaveStatus = "clean" | "dirty" | "saving" | "saved" | "error"; type FlushSave = () => Promise; const pendingSaves = new Set(); export function registerPendingSave(flush: FlushSave) { pendingSaves.add(flush); return () => { pendingSaves.delete(flush); }; } export async function flushPendingSaves() { await Promise.all([...pendingSaves].map((flush) => flush())); } export function useDebouncedAutoSave({ save, delay = 1000, }: { save: () => Promise; delay?: number; }) { const [status, setStatus] = useState("clean"); const [error, setError] = useState(null); const timerRef = useRef | null>(null); const revisionRef = useRef(0); const savedRevisionRef = useRef(0); const savingRef = useRef | null>(null); const saveRef = useRef(save); useEffect(() => { saveRef.current = save; }, [save]); const runSave = useCallback(async () => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } if (savingRef.current) { await savingRef.current; if (revisionRef.current === savedRevisionRef.current) return; } while (revisionRef.current !== savedRevisionRef.current) { const revision = revisionRef.current; setStatus("saving"); setError(null); const request = saveRef.current() .then(() => { savedRevisionRef.current = revision; setStatus(revisionRef.current === revision ? "saved" : "dirty"); }) .catch((reason: unknown) => { setError(reason instanceof Error ? reason.message : "Could not save changes."); setStatus("error"); throw reason; }) .finally(() => { savingRef.current = null; }); savingRef.current = request; await request; } }, []); const schedule = useCallback(() => { revisionRef.current += 1; setStatus("dirty"); setError(null); if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { void runSave().catch(() => undefined); }, delay); }, [delay, runSave]); const flush = useCallback(async () => { if ( revisionRef.current === savedRevisionRef.current && !savingRef.current ) { return; } await runSave(); }, [runSave]); useEffect(() => registerPendingSave(flush), [flush]); useEffect(() => { const handleBeforeUnload = (event: BeforeUnloadEvent) => { if ( revisionRef.current !== savedRevisionRef.current || savingRef.current ) { event.preventDefault(); } }; window.addEventListener("beforeunload", handleBeforeUnload); return () => { window.removeEventListener("beforeunload", handleBeforeUnload); if (timerRef.current) clearTimeout(timerRef.current); if (revisionRef.current !== savedRevisionRef.current) { void saveRef.current().catch(() => undefined); } }; }, []); return { status, error, schedule, flush, retry: runSave }; }