mirror of
https://github.com/Tony0410/quietthanks.git
synced 2026-05-25 05:41:38 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
|
|||||||
const [isLoading, setIsLoading] = useState(!isNewEntry);
|
const [isLoading, setIsLoading] = useState(!isNewEntry);
|
||||||
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||||||
const pendingSaveRef = useRef(false);
|
const pendingSaveRef = useRef(false);
|
||||||
|
const savingRef = useRef(false); // Prevent concurrent saves
|
||||||
|
const entryIdRef = useRef<string | null>(null); // Track entry ID across saves
|
||||||
|
|
||||||
// Load entry (only if editing existing or viewing by date)
|
// Load entry (only if editing existing or viewing by date)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,6 +47,7 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
|
|||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setEntry(data);
|
setEntry(data);
|
||||||
|
entryIdRef.current = data.id;
|
||||||
setText(data.text);
|
setText(data.text);
|
||||||
setMood(data.mood);
|
setMood(data.mood);
|
||||||
setRoughDay(Boolean(data.roughDay));
|
setRoughDay(Boolean(data.roughDay));
|
||||||
@@ -64,12 +67,19 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
|
|||||||
const save = useCallback(async () => {
|
const save = useCallback(async () => {
|
||||||
if (!text.trim()) return null;
|
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);
|
setIsSaving(true);
|
||||||
pendingSaveRef.current = false;
|
pendingSaveRef.current = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body: CreateEntryRequest = {
|
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,
|
date: entry?.date || targetDate,
|
||||||
text: text.trim(),
|
text: text.trim(),
|
||||||
mood,
|
mood,
|
||||||
@@ -86,16 +96,28 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const saved: EntryWithTags = await res.json();
|
const saved: EntryWithTags = await res.json();
|
||||||
setEntry(saved);
|
setEntry(saved);
|
||||||
|
entryIdRef.current = saved.id; // Update ref immediately
|
||||||
setLastSaved(new Date());
|
setLastSaved(new Date());
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save entry:", error);
|
console.error("Failed to save entry:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
savingRef.current = false;
|
||||||
setIsSaving(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;
|
return null;
|
||||||
}, [text, mood, roughDay, tagNames, entry?.id, entry?.date, targetDate]);
|
}, [text, mood, roughDay, tagNames, entry?.date, targetDate]);
|
||||||
|
|
||||||
// Debounced save
|
// Debounced save
|
||||||
const debouncedSave = useDebounce(() => {
|
const debouncedSave = useDebounce(() => {
|
||||||
@@ -157,12 +179,14 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
|
|||||||
// Reset form for new entry
|
// Reset form for new entry
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setEntry(null);
|
setEntry(null);
|
||||||
|
entryIdRef.current = null; // Clear the ID ref
|
||||||
setText("");
|
setText("");
|
||||||
setMood(null);
|
setMood(null);
|
||||||
setRoughDay(false);
|
setRoughDay(false);
|
||||||
setTagNames([]);
|
setTagNames([]);
|
||||||
setLastSaved(null);
|
setLastSaved(null);
|
||||||
pendingSaveRef.current = false;
|
pendingSaveRef.current = false;
|
||||||
|
savingRef.current = false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Save on unmount if pending
|
// Save on unmount if pending
|
||||||
@@ -174,7 +198,7 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: entry?.id,
|
id: entryIdRef.current || undefined,
|
||||||
date: entry?.date || targetDate,
|
date: entry?.date || targetDate,
|
||||||
text: text.trim(),
|
text: text.trim(),
|
||||||
mood,
|
mood,
|
||||||
@@ -184,7 +208,7 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [text, mood, roughDay, tagNames, entry?.id, entry?.date, targetDate]);
|
}, [text, mood, roughDay, tagNames, entry?.date, targetDate]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entry,
|
entry,
|
||||||
|
|||||||
Reference in New Issue
Block a user