fix: discard the value as of the click, not as of its turn

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 10:14:03 +02:00
co-authored by Claude Opus 5
parent 89170ab042
commit 6cbf34c045
2 changed files with 31 additions and 0 deletions
+5
View File
@@ -472,8 +472,13 @@ 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
return this.run(async () => {
if (!this.loaded || this.retired) return { removed: false }
if (this.edits !== askedAt) return { removed: false }
if (this.origin === 'draft') {
const kept = snapshot(this.value)
const keptRow = this.row
+26
View File
@@ -493,6 +493,32 @@ describe('item store: origins', () => {
expect(rows.writes).toEqual([{ path: 'u/me/r', value: null }])
})
it('keeps an edit typed after a discard was asked for, while it waited its turn', 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()
item.value = { ...deployedRes, description: 'typed after the discard' }
gate.resolve()
expect(await saving).toMatchObject({ ok: true })
// The discard is older than what is on screen: reverting to `deployed` would drop it.
expect(await discarding).toEqual({ removed: false })
expect(item.value).toEqual({ ...deployedRes, description: 'typed after the discard' })
expect(item.dirty).toBe(true)
expect(rows.writes.at(-1)).toEqual({
path: 'u/me/r',
value: { ...deployedRes, description: 'typed after the discard' }
})
})
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' }