fix: let a discard asked before a move lands win over the edits it drops

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vn1jCHUcAdaG9C4NsUehF
This commit is contained in:
Ruben Fiszel
2026-09-14 19:12:23 +02:00
co-authored by Claude Opus 5
parent 984e958a72
commit 4ffa65538f
2 changed files with 37 additions and 1 deletions
+7 -1
View File
@@ -180,6 +180,8 @@ class Entry<V> {
/** 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<unknown> | undefined
/** The entries a save of this entry waits for before moving: a move skips waiting for any
@@ -421,7 +423,9 @@ class Entry<V> {
* 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<V>): 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<V> {
}
discard(): Promise<DiscardOutcome> {
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)
+30
View File
@@ -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()