105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { registerPendingSave } from "@/lib/admin-form-save-bridge";
|
|
|
|
export type AutoSaveStatus = "clean" | "dirty" | "saving" | "saved" | "error";
|
|
|
|
export function useDebouncedAutoSave({
|
|
save,
|
|
delay = 1000,
|
|
}: {
|
|
save: () => Promise<void>;
|
|
delay?: number;
|
|
}) {
|
|
const [status, setStatus] = useState<AutoSaveStatus>("clean");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const revisionRef = useRef(0);
|
|
const savedRevisionRef = useRef(0);
|
|
const savingRef = useRef<Promise<void> | 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 };
|
|
}
|