fix: keep a draft written at a key while a save is moving onto it

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 18:36:44 +02:00
co-authored by Claude Opus 5
parent 981ba6f882
commit aca3aef0ec
2 changed files with 48 additions and 3 deletions
+22 -3
View File
@@ -757,6 +757,9 @@ export function createItemStore(ports: ItemRowPort) {
const entries = new Map<string, Entry<any>>()
/** The latest save moving onto a key, by that key, and when its move is done. */
const moves = new Map<string, { owner: Entry<any>; done: Promise<void> }>()
/** Drafts written from outside at a key while a save is moving onto it: newer than that write,
* so the entry arriving there takes them rather than clearing the row it finds. */
const arrivals = new Map<string, unknown>()
const internals: StoreInternals = {
release(entry) {
@@ -775,6 +778,11 @@ export function createItemStore(ports: ItemRowPort) {
const displaced = entries.get(to)
if (displaced && displaced !== entry) retire(displaced, entry)
entries.set(to, entry)
const arrived = arrivals.get(to)
if (arrived !== undefined) {
arrivals.delete(to)
entry.applyExternal(arrived)
}
},
/**
* A save about to write the item at `key` and move onto it. It goes after the command
@@ -804,7 +812,9 @@ export function createItemStore(ports: ItemRowPort) {
turn: Promise.all(waits).then(() => (claimant.waitingOn = [])),
release() {
release()
if (moves.get(k) === move) moves.delete(k)
if (moves.get(k) !== move) return
moves.delete(k)
arrivals.delete(k)
}
}
},
@@ -929,8 +939,14 @@ export function createItemStore(ports: ItemRowPort) {
const bridge = {
seed(workspace: string, kind: UserDraftItemKind, path: string, value: unknown): boolean {
if (value === undefined || value === null) return false
const entry = find(workspace, kind, path)
if (!entry || value === undefined || value === null) return false
if (!entry) {
// Persisted as usual, and handed to the save moving onto this key when it lands.
const k = keyString({ workspace, kind: kind as ItemKind, path })
if (moves.has(k)) arrivals.set(k, snapshot(value))
return false
}
entry.applyExternal(value)
return true
},
@@ -946,7 +962,10 @@ export function createItemStore(ports: ItemRowPort) {
},
discard(workspace: string, kind: UserDraftItemKind, path: string): boolean {
const entry = find(workspace, kind, path)
if (!entry) return false
if (!entry) {
arrivals.delete(keyString({ workspace, kind: kind as ItemKind, path }))
return false
}
void entry.discard()
return true
},
+26
View File
@@ -530,6 +530,32 @@ describe('item store: one entry per key', () => {
expect(rows.writes.at(-1)).toEqual({ path: 'u/me/b', value: kept })
})
it('keeps a draft written from outside at the key a save is moving onto', 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 chat = { ...b, description: 'written by the chat meanwhile' }
// Nobody holds the key yet, so the caller persists it itself.
expect(store.bridge.seed('w', 'resource', 'u/me/b', chat)).toBe(false)
gate.resolve()
expect(await moved).toMatchObject({ ok: true, moved: true })
expect(moving.deployed).toEqual({ ...b, args: { a: 2 } })
expect(moving.value).toEqual(chat)
expect(rows.writes.at(-1)).toEqual({ path: 'u/me/b', value: chat })
})
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()