From 25480bc7f27fbb57eb46f009f471f23f25f0dddc Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 7 Sep 2026 10:30:28 +0200 Subject: [PATCH] fix(drafts): keep a released cell readable for every save still in flight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two saves of the same cell can be in flight at once, and consuming the record on the first settle left the second reading the released cell as "nothing newer" — so it deleted the edit the first had just preserved. The window that recorded it already owns its lifetime and drops it when the last save closes, so the read does not have to. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4 --- frontend/src/lib/userDraft.svelte.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/userDraft.svelte.ts b/frontend/src/lib/userDraft.svelte.ts index 9c7f545e36..2684c64220 100644 --- a/frontend/src/lib/userDraft.svelte.ts +++ b/frontend/src/lib/userDraft.svelte.ts @@ -175,16 +175,17 @@ export function beginDraftSettleWindow( releasedValues.delete(mk) } } -/** Read once: what it describes is settled by the caller that reads it. */ -function takeReleasedValue( +/** + * Only reads: the window that recorded it owns its lifetime. Two saves of the same + * cell can be in flight at once, and the second settle has to see the same edit the + * first did, or it reads the released cell as "nothing newer" and deletes it. + */ +function releasedValue( itemKind: UserDraftItemKind, path: string, opts?: UserDraftOptions ): V | undefined { - const mk = mapKey(resolveWorkspace(opts), itemKind, path) - const value = releasedValues.get(mk) as V | undefined - releasedValues.delete(mk) - return value + return releasedValues.get(mapKey(resolveWorkspace(opts), itemKind, path)) as V | undefined } const liveEditorDrafts = new Map() @@ -338,7 +339,7 @@ export async function settleDraftAfterWrite( // nothing is watching any more — except when the release itself is what took // the handle away mid-write, in which case what it held still speaks for the // user. Absent both, there is nothing newer to protect. - const held = live !== undefined ? live : takeReleasedValue(itemKind, fromPath, opts) + const held = live !== undefined ? live : releasedValue(itemKind, fromPath, opts) const diverged = held !== undefined && !draftValuesEqual(held, written) if (savedPath === fromPath && diverged) return // The form stays editable across the delete's own request, so a keystroke made