diff --git a/frontend/src/lib/components/ResourceEditor.svelte b/frontend/src/lib/components/ResourceEditor.svelte index 4311d07424..e9e8adfcaa 100644 --- a/frontend/src/lib/components/ResourceEditor.svelte +++ b/frontend/src/lib/components/ResourceEditor.svelte @@ -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) diff --git a/frontend/src/lib/components/VariableEditor.svelte b/frontend/src/lib/components/VariableEditor.svelte index 17b1630327..9356701d02 100644 --- a/frontend/src/lib/components/VariableEditor.svelte +++ b/frontend/src/lib/components/VariableEditor.svelte @@ -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) diff --git a/frontend/src/lib/userDraftDbSyncer.svelte.ts b/frontend/src/lib/userDraftDbSyncer.svelte.ts index 25125a74e4..9b545f0558 100644 --- a/frontend/src/lib/userDraftDbSyncer.svelte.ts +++ b/frontend/src/lib/userDraftDbSyncer.svelte.ts @@ -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 { + 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) } }