fix: do not let a clean editor acknowledge a row it never knew of

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 21:00:05 +02:00
co-authored by Claude Opus 5
parent 6e0a9a96b3
commit a5a4f25751
2 changed files with 27 additions and 2 deletions
+6 -2
View File
@@ -522,15 +522,19 @@ class Entry<V> {
* item, so the row is the caller's. `failed`: has it, on a baseline it cannot refresh. */
async discarded(): Promise<'done' | 'absent' | 'failed'> {
const asked = this.asOf()
// A row this entry has none of is not one it can discard: it does nothing, and the caller
// would read that as the row being gone while another tab's is still there.
let knewRow = this.row !== null
await this.discard(asked)
if (this.retired || !this.loaded) return 'absent'
if (!this.stale) return 'done'
if (!this.stale) return knewRow ? 'done' : 'absent'
// Stale refused it. A reload is what clears that, and with a baseline it can trust the
// discard means something again — so the one thing worth trying before giving up. The
// retry carries the state as of the request, so an edit typed during that reload stands.
if (!this.adapter || !(await this.load(this.adapter)).ok) return 'failed'
knewRow = this.row !== null
await this.discard(asked)
return this.stale ? 'failed' : 'done'
return this.stale ? 'failed' : knewRow ? 'done' : 'absent'
}
/**
+21
View File
@@ -1127,6 +1127,27 @@ describe('item store: origins', () => {
})
})
it('says a row it never knew of is not its to discard', async () => {
const rows = fakeRows()
const store = createItemStore(rows.port)
// Loaded clean: no draft existed when this editor read the item.
const { handle: item } = store.acquire(
{ workspace: 'w', kind: 'resource', path: 'u/me/r' },
{ workspace: 'w', path: 'u/me/r' },
adapter({ deployed: deployedRes })
)
await settle()
expect(item.dirty).toBe(false)
// Another tab creates the draft afterwards. This editor knows nothing of it.
rows.sent.set('u/me/r', { ...deployedRes, description: 'the other tab' })
rows.handExternally('u/me/r')
// Its discard does nothing, so answering `done` would have the caller skip the delete and
// report a draft gone that is still on the server.
expect(await store.bridge.discard('w', 'resource', 'u/me/r')).toBe('absent')
})
it('says it did not deal with the row when its own first read never arrived', async () => {
const rows = fakeRows()
const store = createItemStore(rows.port)