mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-26 00:03:59 +00:00
fix: carry a discard's original snapshot through its recovery retry
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
514685b65f
commit
03b62f46db
@@ -481,13 +481,15 @@ class Entry<V> {
|
||||
* was asked to throw away. `absent`: never had the item, so the row is the caller's.
|
||||
* `failed`: it has the item, on a baseline it cannot refresh, and still shows the draft. */
|
||||
async discarded(): Promise<'done' | 'absent' | 'failed'> {
|
||||
await this.discard()
|
||||
const asked = this.asOf()
|
||||
await this.discard(asked)
|
||||
if (this.retired || !this.loaded) return 'absent'
|
||||
if (!this.stale) return 'done'
|
||||
// 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.
|
||||
// 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'
|
||||
await this.discard()
|
||||
await this.discard(asked)
|
||||
return this.stale ? 'failed' : 'done'
|
||||
}
|
||||
|
||||
@@ -615,7 +617,10 @@ class Entry<V> {
|
||||
if (changed) this.replaceValue(next as V)
|
||||
}
|
||||
|
||||
discard(): Promise<DiscardOutcome> {
|
||||
/** `asked`: for a caller retrying after recovering the item, the state as of the *first*
|
||||
* attempt. Taking a fresh one would let an edit made during that recovery look like part of
|
||||
* what the discard was asked to throw away. */
|
||||
discard(asked?: AsOf): Promise<DiscardOutcome> {
|
||||
return this.run(async (at) => {
|
||||
if (!this.loaded || this.retired) return { removed: false }
|
||||
// Reverting to a baseline known to be behind the server would write it back.
|
||||
@@ -655,7 +660,7 @@ class Entry<V> {
|
||||
this.reconcile()
|
||||
await this.settleRows([this.key])
|
||||
return { removed: false }
|
||||
})
|
||||
}, asked)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -831,7 +836,9 @@ export type ItemHandle<V> = {
|
||||
readonly pristine: boolean
|
||||
readonly revision: number
|
||||
readonly meta: unknown
|
||||
/** `dirty && valid && writable && !busy`. */
|
||||
/** `dirty && valid && writable && !busy`, and not stale: a write landed on this item
|
||||
* elsewhere and the re-read meant to pick it up failed, so saving would put a baseline known
|
||||
* to be behind the server back over it. A reload clears that. */
|
||||
readonly canSave: boolean
|
||||
save(): Promise<SaveOutcome>
|
||||
discard(): Promise<DiscardOutcome>
|
||||
|
||||
@@ -1386,6 +1386,43 @@ describe('item store: conflicts', () => {
|
||||
expect(item.status).not.toBe('conflicted')
|
||||
})
|
||||
|
||||
it('keeps an edit typed while a stale discard was recovering the item', async () => {
|
||||
const rows = fakeRows()
|
||||
const store = createItemStore(rows.port)
|
||||
const recovery = deferred<ItemLoad<Res>>()
|
||||
let reads = 0
|
||||
const key: ItemKey = { workspace: 'w', kind: 'resource', path: 'u/me/r' }
|
||||
const { handle: item } = store.acquire(
|
||||
key,
|
||||
{ workspace: 'w', path: 'u/me/r' },
|
||||
adapter(() => {
|
||||
reads++
|
||||
if (reads === 1) return Promise.resolve({ deployed: deployedRes })
|
||||
if (reads === 2) return Promise.reject(new Error('the GET failed'))
|
||||
return recovery.promise
|
||||
})
|
||||
)
|
||||
await settle()
|
||||
item.value = { ...deployedRes, description: 'mine' }
|
||||
// A write landed elsewhere and the re-read for it failed, so this editor is stale.
|
||||
expect(await store.bridge.refresh('w', 'resource', 'u/me/r')).toBe('failed')
|
||||
|
||||
// The chat discards. That is refused while stale, so it reloads to recover — and the
|
||||
// user types while that GET is out.
|
||||
const discarding = store.bridge.discard('w', 'resource', 'u/me/r')
|
||||
await settle()
|
||||
item.value = { ...deployedRes, description: 'typed during the recovery' }
|
||||
recovery.resolve({ deployed: deployedRes })
|
||||
|
||||
// The retry is about the value the chat named, not this one, so the edit stands.
|
||||
expect(await discarding).toBe('done')
|
||||
expect(item.value?.description).toBe('typed during the recovery')
|
||||
expect(rows.writes.at(-1)).toEqual({
|
||||
path: 'u/me/r',
|
||||
value: { ...deployedRes, description: 'typed during the recovery' }
|
||||
})
|
||||
})
|
||||
|
||||
it('stops an editor acting on a baseline its failed re-read left behind', async () => {
|
||||
const rows = fakeRows()
|
||||
const store = createItemStore(rows.port)
|
||||
|
||||
Reference in New Issue
Block a user