feat: add Redis-backed SSE updates

This commit is contained in:
2026-07-24 21:31:47 +07:00 Unverified
parent 5cc7edaa13
commit 29df784aeb
22 changed files with 912 additions and 96 deletions
+58 -2
View File
@@ -78,6 +78,14 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
const formTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const questionsTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const formDirtyRef = useRef(false);
const questionsDirtyRef = useRef(false);
const formSavingRef = useRef(false);
const questionsSavingRef = useRef(false);
const formRevisionRef = useRef(0);
const questionsRevisionRef = useRef(0);
const activeFormSaveRef = useRef(0);
const activeQuestionsSaveRef = useRef(0);
// Refs to always read latest state inside debounced callbacks
const formTitleRef = useRef(formTitle);
@@ -98,6 +106,9 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
formTimerRef.current = setTimeout(async () => {
const title = formTitleRef.current;
if (!title.trim()) return;
const revision = formRevisionRef.current;
activeFormSaveRef.current = revision;
formSavingRef.current = true;
setFormSaveStatus("saving");
try {
await updateForm(form.id, {
@@ -105,12 +116,19 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
description: formDescriptionRef.current,
isOpen: formIsOpenRef.current,
});
if (formRevisionRef.current === revision) {
formDirtyRef.current = false;
}
setFormSaveStatus("saved");
router.refresh();
setTimeout(() => setFormSaveStatus("idle"), 2000);
} catch {
toast.error("Failed to auto-save form settings.");
setFormSaveStatus("idle");
} finally {
if (activeFormSaveRef.current === revision) {
formSavingRef.current = false;
}
}
}, 1000);
}, [form.id, router]);
@@ -121,6 +139,9 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
questionsTimerRef.current = setTimeout(async () => {
const current = questionsRef.current;
if (current.length === 0) return;
const revision = questionsRevisionRef.current;
activeQuestionsSaveRef.current = revision;
questionsSavingRef.current = true;
setQuestionsSaveStatus("saving");
try {
const formattedUpdates = current.map(q => ({
@@ -129,12 +150,19 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
allowOther: q.allowOther,
}));
await bulkUpdateQuestions(form.id, formattedUpdates);
if (questionsRevisionRef.current === revision) {
questionsDirtyRef.current = false;
}
setQuestionsSaveStatus("saved");
router.refresh();
setTimeout(() => setQuestionsSaveStatus("idle"), 2000);
} catch {
toast.error("Failed to auto-save questions.");
setQuestionsSaveStatus("idle");
} finally {
if (activeQuestionsSaveRef.current === revision) {
questionsSavingRef.current = false;
}
}
}, 1500);
}, [form.id, router]);
@@ -148,16 +176,22 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
}, []);
const handleFormTitleChange = (val: string) => {
formDirtyRef.current = true;
formRevisionRef.current += 1;
setFormTitle(val);
autoSaveForm();
};
const handleFormDescriptionChange = (val: string) => {
formDirtyRef.current = true;
formRevisionRef.current += 1;
setFormDescription(val);
autoSaveForm();
};
const handleFormIsOpenChange = (val: boolean) => {
formDirtyRef.current = true;
formRevisionRef.current += 1;
setFormIsOpen(val);
autoSaveForm();
};
@@ -179,6 +213,8 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
};
const handleUpdateQuestion = (id: string, updates: Partial<Question>) => {
questionsDirtyRef.current = true;
questionsRevisionRef.current += 1;
setQuestions((prev) =>
prev.map((q) => (q.id === id ? { ...q, ...updates } as Question : q))
);
@@ -188,6 +224,11 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
const handleDeleteQuestion = async (id: string) => {
try {
await deleteQuestion(id, form.id);
setQuestions((current) => {
const next = current.filter((question) => question.id !== id);
questionsRef.current = next;
return next;
});
router.refresh();
} catch (err: unknown) {
console.error("Failed to delete question:", err);
@@ -208,6 +249,8 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
q.displayOrder = idx;
});
questionsDirtyRef.current = true;
questionsRevisionRef.current += 1;
setQuestions(newQuestions);
autoSaveQuestions();
};
@@ -287,8 +330,21 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
};
useEffect(() => {
const timeout = setTimeout(() => setQuestions(initialQuestions), 0);
return () => clearTimeout(timeout);
if (formDirtyRef.current || formSavingRef.current) return;
setFormTitle(form.title);
setFormDescription(form.description || "");
setFormIsOpen(form.isOpen);
formTitleRef.current = form.title;
formDescriptionRef.current = form.description || "";
formIsOpenRef.current = form.isOpen;
}, [form]);
useEffect(() => {
if (questionsDirtyRef.current || questionsSavingRef.current) return;
setQuestions(initialQuestions);
questionsRef.current = initialQuestions;
}, [initialQuestions]);
return (