Skip to content

Commit 0cca61e

Browse files
committed
fix(files): chain autosave unmount flush after in-flight save
The unmount flush no longer fires a concurrent PUT alongside an in-flight save; it awaits the in-flight save and then writes the latest content sequentially, so an out-of-order completion can't clobber newer edits with a stale snapshot (addresses Cursor Bugbot).
1 parent a53bec9 commit 0cca61e

1 file changed

Lines changed: 39 additions & 24 deletions

File tree

apps/sim/hooks/use-autosave.ts

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export function useAutosave({
4646

4747
const isDirty = content !== savedContent
4848
const savingStartRef = useRef(0)
49+
const inFlightRef = useRef<Promise<void> | null>(null)
50+
const unmountedRef = useRef(false)
4951
const MIN_SAVING_DISPLAY_MS = 600
5052

5153
const save = useCallback(async () => {
@@ -58,25 +60,33 @@ export function useAutosave({
5860
}
5961
savingRef.current = true
6062
savingStartRef.current = Date.now()
61-
setSaveStatus('saving')
62-
let nextStatus: SaveStatus = 'saved'
63-
try {
64-
await onSaveRef.current()
65-
} catch {
66-
nextStatus = 'error'
67-
} finally {
68-
const elapsed = Date.now() - savingStartRef.current
69-
const remaining = Math.max(0, MIN_SAVING_DISPLAY_MS - elapsed)
70-
displayTimerRef.current = setTimeout(() => {
71-
setSaveStatus(nextStatus)
72-
clearTimeout(idleTimerRef.current)
73-
idleTimerRef.current = setTimeout(() => setSaveStatus('idle'), 2000)
74-
savingRef.current = false
75-
if (nextStatus !== 'error' && contentRef.current !== savedContentRef.current) {
76-
save()
63+
if (!unmountedRef.current) setSaveStatus('saving')
64+
const run = (async () => {
65+
let nextStatus: SaveStatus = 'saved'
66+
try {
67+
await onSaveRef.current()
68+
} catch {
69+
nextStatus = 'error'
70+
} finally {
71+
if (unmountedRef.current) {
72+
savingRef.current = false
73+
} else {
74+
const elapsed = Date.now() - savingStartRef.current
75+
const remaining = Math.max(0, MIN_SAVING_DISPLAY_MS - elapsed)
76+
displayTimerRef.current = setTimeout(() => {
77+
setSaveStatus(nextStatus)
78+
clearTimeout(idleTimerRef.current)
79+
idleTimerRef.current = setTimeout(() => setSaveStatus('idle'), 2000)
80+
savingRef.current = false
81+
if (nextStatus !== 'error' && contentRef.current !== savedContentRef.current) {
82+
save()
83+
}
84+
}, remaining)
7785
}
78-
}, remaining)
79-
}
86+
}
87+
})()
88+
inFlightRef.current = run
89+
await run
8090
}, [])
8191

8292
useEffect(() => {
@@ -88,15 +98,20 @@ export function useAutosave({
8898

8999
useEffect(() => {
90100
return () => {
101+
unmountedRef.current = true
91102
clearTimeout(timerRef.current)
92103
clearTimeout(idleTimerRef.current)
93104
clearTimeout(displayTimerRef.current)
94-
// Flush the latest content on unmount even if a save is mid-flight: that in-flight save
95-
// captured an older snapshot, so skipping here would terminally drop any edit typed since.
96-
// The duplicate PUT is idempotent.
97-
if (enabledRef.current && contentRef.current !== savedContentRef.current) {
98-
onSaveRef.current().catch(() => {})
99-
}
105+
if (!enabledRef.current || contentRef.current === savedContentRef.current) return
106+
// Flush the latest content on unmount, but chain it AFTER any in-flight save rather than
107+
// firing a concurrent PUT: the in-flight save captured an older snapshot, so writing the
108+
// latest sequentially (last) prevents an out-of-order completion from clobbering it.
109+
void (async () => {
110+
await inFlightRef.current
111+
if (contentRef.current !== savedContentRef.current) {
112+
await onSaveRef.current().catch(() => {})
113+
}
114+
})()
100115
}
101116
}, [])
102117

0 commit comments

Comments
 (0)