diff --git a/frontend/src/lib/itemStore.svelte.ts b/frontend/src/lib/itemStore.svelte.ts index 91a127356c..fe784da4ef 100644 --- a/frontend/src/lib/itemStore.svelte.ts +++ b/frontend/src/lib/itemStore.svelte.ts @@ -470,9 +470,13 @@ class Entry { if (this.origin === 'draft') { const kept = snapshot(this.value) const keptRow = this.row + const editsAtStart = this.edits this.replaceValue(undefined) this.reconcile() await this.settleRows([this.key]) + // An outside write that arrived while the delete was going is newer than this + // discard: it has already put the item back, and its row is the item. + if (this.edits !== editsAtStart) return { removed: false } // The row is the item: while its delete has not landed, the item is still there. if (this.rowRefused(this.key)) { // The syncer keeps a failed payload for its page-close flush; that delete would diff --git a/frontend/src/lib/itemStore.test.ts b/frontend/src/lib/itemStore.test.ts index dbb3cd80b5..29b00a14c4 100644 --- a/frontend/src/lib/itemStore.test.ts +++ b/frontend/src/lib/itemStore.test.ts @@ -435,6 +435,25 @@ describe('item store: origins', () => { expect(rows.writes).toEqual([{ path: 'u/me/r', value: null }]) }) + it('keeps a draft-only item an outside write put back while its delete was going', async () => { + const rows = fakeRows() + const draft = { ...deployedRes, description: 'only a draft' } + const { item } = await open(rows, adapter({ draft })) + const gate = deferred() + rows.hold(gate.promise) + const discarded = item.discard() + await settle() + // The chat writes the same item while the delete is in flight. + const fromChat = { ...draft, description: 'written by the chat' } + item.applyExternal(fromChat) + gate.resolve() + + expect(await discarded).toEqual({ removed: false }) + expect(item.removed).toBe(false) + expect(item.value).toEqual(fromChat) + expect(rows.writes.at(-1)).toEqual({ path: 'u/me/r', value: fromChat }) + }) + it('keeps a draft-only item whose delete did not land', async () => { const rows = fakeRows() const draft = { ...deployedRes, description: 'only a draft' }