diff --git a/frontend/src/lib/itemStore.svelte.ts b/frontend/src/lib/itemStore.svelte.ts index 6a1f06de7f..2a678f09b1 100644 --- a/frontend/src/lib/itemStore.svelte.ts +++ b/frontend/src/lib/itemStore.svelte.ts @@ -472,13 +472,15 @@ class Entry { } discard(): Promise { - // What a discard throws away is the value as of the click, like a save sends the value as - // of the click: one typed while it waited its turn behind a save is newer than it, and - // reverting that would drop it with nothing left to recover it from. - const askedAt = this.edits + // What a discard throws away is the value as of the click, the way a save sends the value + // as of the click. One typed while it waited its turn is newer than the discard, and + // reverting it would drop it with nothing left holding it. Measured as the value and not + // as a count of writes: an outside write of what is already on screen changes nothing the + // user can see, so it does not make their discard stale. + const asked = serialize(this.value) return this.run(async () => { if (!this.loaded || this.retired) return { removed: false } - if (this.edits !== askedAt) return { removed: false } + if (serialize(this.value) !== asked) return { removed: false } if (this.origin === 'draft') { const kept = snapshot(this.value) const keptRow = this.row diff --git a/frontend/src/lib/itemStore.test.ts b/frontend/src/lib/itemStore.test.ts index 2a8a310a45..c9555c3bf7 100644 --- a/frontend/src/lib/itemStore.test.ts +++ b/frontend/src/lib/itemStore.test.ts @@ -519,6 +519,29 @@ describe('item store: origins', () => { }) }) + it('discards behind an outside write of the value already on screen', async () => { + const rows = fakeRows() + const gate = deferred() + const { item } = await open( + rows, + adapter({ deployed: deployedRes }, () => gate.promise) + ) + item.value = { ...deployedRes, description: 'edited' } + const saving = item.save() + await settle() + const discarding = item.discard() + await settle() + // The chat saves what the user is already looking at: nothing on screen moves, so the + // discard is still about the value its click named. + item.applyExternal({ ...deployedRes, description: 'edited' }) + gate.resolve() + + expect(await saving).toMatchObject({ ok: true }) + expect(await discarding).toEqual({ removed: false }) + expect(item.dirty).toBe(false) + expect(rows.writes.at(-1)).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' }