fix: measure a queued discard against the value it named, not a write count

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 10:31:53 +02:00
co-authored by Claude Opus 5
parent 6cbf34c045
commit 8a6fa9b8a4
2 changed files with 30 additions and 5 deletions
+7 -5
View File
@@ -472,13 +472,15 @@ class Entry<V> {
}
discard(): Promise<DiscardOutcome> {
// 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
+23
View File
@@ -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' }