From e99401858fe2e2221417e083c8e4df535dafc0ed Mon Sep 17 00:00:00 2001 From: Gemini Agent Date: Sat, 24 Jan 2026 12:08:33 +0000 Subject: [PATCH] Fix race condition causing duplicate entries during autosave Use refs to track entry ID and prevent concurrent saves. When a save is in progress, queue the next save instead of starting a new one. Co-Authored-By: Claude Opus 4.5 --- src/hooks/useEntry.ts | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/hooks/useEntry.ts b/src/hooks/useEntry.ts index f6ccb99..0bee40f 100644 --- a/src/hooks/useEntry.ts +++ b/src/hooks/useEntry.ts @@ -23,6 +23,8 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions const [isLoading, setIsLoading] = useState(!isNewEntry); const [lastSaved, setLastSaved] = useState(null); const pendingSaveRef = useRef(false); + const savingRef = useRef(false); // Prevent concurrent saves + const entryIdRef = useRef(null); // Track entry ID across saves // Load entry (only if editing existing or viewing by date) useEffect(() => { @@ -45,6 +47,7 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions if (data) { setEntry(data); + entryIdRef.current = data.id; setText(data.text); setMood(data.mood); setRoughDay(Boolean(data.roughDay)); @@ -64,12 +67,19 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions const save = useCallback(async () => { if (!text.trim()) return null; + // Prevent concurrent saves + if (savingRef.current) { + pendingSaveRef.current = true; // Mark that we need to save again + return null; + } + + savingRef.current = true; setIsSaving(true); pendingSaveRef.current = false; try { const body: CreateEntryRequest = { - id: entry?.id, // Include id if updating existing + id: entryIdRef.current || undefined, // Use ref to always have latest ID date: entry?.date || targetDate, text: text.trim(), mood, @@ -86,16 +96,28 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions if (res.ok) { const saved: EntryWithTags = await res.json(); setEntry(saved); + entryIdRef.current = saved.id; // Update ref immediately setLastSaved(new Date()); return saved; } } catch (error) { console.error("Failed to save entry:", error); } finally { + savingRef.current = false; setIsSaving(false); + + // If there were changes while saving, trigger another save + if (pendingSaveRef.current) { + pendingSaveRef.current = false; + // Use setTimeout to avoid immediate recursion + setTimeout(() => { + pendingSaveRef.current = true; + debouncedSave(); + }, 100); + } } return null; - }, [text, mood, roughDay, tagNames, entry?.id, entry?.date, targetDate]); + }, [text, mood, roughDay, tagNames, entry?.date, targetDate]); // Debounced save const debouncedSave = useDebounce(() => { @@ -157,12 +179,14 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions // Reset form for new entry const reset = useCallback(() => { setEntry(null); + entryIdRef.current = null; // Clear the ID ref setText(""); setMood(null); setRoughDay(false); setTagNames([]); setLastSaved(null); pendingSaveRef.current = false; + savingRef.current = false; }, []); // Save on unmount if pending @@ -174,7 +198,7 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - id: entry?.id, + id: entryIdRef.current || undefined, date: entry?.date || targetDate, text: text.trim(), mood, @@ -184,7 +208,7 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions }).catch(() => {}); } }; - }, [text, mood, roughDay, tagNames, entry?.id, entry?.date, targetDate]); + }, [text, mood, roughDay, tagNames, entry?.date, targetDate]); return { entry,