fix: ignore a reload the editor has moved on from, and settle saves before resolving

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-19 23:36:06 +02:00
co-authored by Claude Opus 5
parent cb14f51445
commit 20e4424925
3 changed files with 37 additions and 8 deletions
@@ -303,10 +303,17 @@
labels: r.labels ?? undefined,
wsSpecific: r.ws_specific ?? false
}
// Dropped after the read and before the baseline below: anything an autosave queued
// while it was out belongs to the version being replaced, and leaving it parked would
// let the fresh baseline make it acceptable — sending it over the version just chosen.
UserDraftDbSyncer.dropPending(query)
// Everything below writes shared editor state, so first make sure it is still this
// resource's: the drawer stays closable while the read is out, and another resource
// opened meanwhile would otherwise get this one's baseline — and with it this one's
// path as its save target.
if (selected !== ws || initialPath !== p) return
// Anything an autosave queued while the read was out belongs to the version being
// replaced. Dropping is not enough on its own: a POST the runner already started
// cannot be cancelled, and if it settles after the baseline below, its rejection
// raises the conflict again. So wait for the chain to go quiet first.
await UserDraftDbSyncer.quiesce(query)
if (selected !== ws || initialPath !== p) return
UserDraftDbSyncer.clearConflict(query)
initialStates[ws] = structuredClone(deployedState)
UserDraftDbSyncer.recordRemoteSync(query, (r as any).draft_saved_at)
@@ -174,10 +174,17 @@
labels: v.labels ?? undefined,
wsSpecific: v.ws_specific ?? false
}
// Dropped after the read and before the baseline below: anything an autosave queued
// while it was out belongs to the version being replaced, and leaving it parked would
// let the fresh baseline make it acceptable — sending it over the version just chosen.
UserDraftDbSyncer.dropPending(query)
// Everything below writes shared editor state, so first make sure it is still this
// variable's: the drawer stays closable while the read is out, and another variable
// opened meanwhile would otherwise get this one's baseline — and with it this one's
// path as its save target.
if (selected !== ws || editPath !== p) return
// Anything an autosave queued while the read was out belongs to the version being
// replaced. Dropping is not enough on its own: a POST the runner already started
// cannot be cancelled, and if it settles after the baseline below, its rejection
// raises the conflict again. So wait for the chain to go quiet first.
await UserDraftDbSyncer.quiesce(query)
if (selected !== ws || editPath !== p) return
UserDraftDbSyncer.clearConflict(query)
initialStates[ws] = structuredClone(deployedState)
UserDraftDbSyncer.recordRemoteSync(query, (v as any).draft_saved_at)
@@ -723,5 +723,20 @@ export const UserDraftDbSyncer = {
const key = draftKey(query.workspace, query.itemKind, query.path)
pendingSaveOpts.delete(key)
debouncer.cancel(key)
},
/**
* Drop what is parked and wait until nothing for this key is still in flight. `dropPending`
* alone cannot stop a POST the runner already started, and such a POST settles *after* the
* caller has moved on — a rejected one re-raising the conflict it was told to resolve. Await
* this before installing a baseline that would make a stale payload acceptable.
*/
async quiesce(query: UserDraftLastSyncQuery): Promise<void> {
const key = draftKey(query.workspace, query.itemKind, query.path)
this.dropPending(query)
await runner.settled(key)
// A save that landed while we waited parks its own opts again; they belong to the version
// being replaced, so they go too.
this.dropPending(query)
}
}