diff --git a/frontend/src/lib/itemStore.svelte.ts b/frontend/src/lib/itemStore.svelte.ts index 33cfa84566..e61088812c 100644 --- a/frontend/src/lib/itemStore.svelte.ts +++ b/frontend/src/lib/itemStore.svelte.ts @@ -180,6 +180,8 @@ class Entry { /** Replaced by an entry that moved onto its key: it no longer owns the row there, and a write * reaching its turn does nothing, as its handles show the item that replaced it. */ retired = false + /** Discards asked and still waiting for their turn. */ + discardsAsked = 0 /** The command running now, past its turn: what a move onto this key waits for. */ running: Promise | undefined /** The entries a save of this entry waits for before moving: a move skips waiting for any @@ -421,7 +423,9 @@ class Entry { * changed from its own deployed side, so they stay a draft over what this entry deployed. * Compared exactly, as a save is: a field the draft comparison ignores (run-as) is an edit. */ carryEditsOf(old: Entry): void { - if (old.value === undefined || this.value === undefined) return + // A discard asked before the move landed wins over the edits it was asked to drop; had it + // run first, it would have dropped anything typed after it too. + if (old.value === undefined || this.value === undefined || old.discardsAsked > 0) return // Not loaded yet (its read waits behind the move): only an outside write can have filled it, // and nothing has persisted that write but this entry. if (!old.loaded) { @@ -460,7 +464,9 @@ class Entry { } discard(): Promise { + this.discardsAsked++ return this.run(async () => { + this.discardsAsked-- if (!this.loaded || this.retired) return { removed: false } if (this.origin === 'draft') { const kept = snapshot(this.value) diff --git a/frontend/src/lib/itemStore.test.ts b/frontend/src/lib/itemStore.test.ts index d36108ef46..13e7a79fef 100644 --- a/frontend/src/lib/itemStore.test.ts +++ b/frontend/src/lib/itemStore.test.ts @@ -586,6 +586,36 @@ describe('item store: one entry per key', () => { expect(rows.writes.at(-1)).toEqual({ path: 'u/me/b', value: chat }) }) + it('lets a discard asked before a move lands win over the edits it drops', async () => { + const rows = fakeRows() + const store = createItemStore(rows.port) + const gate = deferred() + const b = { ...deployedRes, path: 'u/me/b' } + const temporary = newItemPath() + const { handle: moving } = store.acquire( + { workspace: 'w', kind: 'resource', path: temporary }, + { workspace: 'w', path: temporary, template: b }, + adapter({}, () => gate.promise) + ) + await settle() + moving.value = { ...b, args: { a: 2 } } + const moved = moving.save() + await settle() + const { handle: opened } = store.acquire( + { workspace: 'w', kind: 'resource', path: 'u/me/b' }, + { workspace: 'w', path: 'u/me/b' }, + adapter({ deployed: b }) + ) + store.bridge.seed('w', 'resource', 'u/me/b', { ...b, description: 'written by the chat' }) + store.bridge.discard('w', 'resource', 'u/me/b') + + gate.resolve() + expect(await moved).toMatchObject({ ok: true, moved: true }) + expect(opened.value).toEqual({ ...b, args: { a: 2 } }) + expect(opened.dirty).toBe(false) + expect(rows.writes.at(-1)).toEqual({ path: 'u/me/b', value: null }) + }) + it('keeps a change the draft comparison ignores in an editor a move replaces', async () => { type Sched = { path: string; summary: string; permissioned_as?: string } const rows = fakeRows()