Files
windmill/frontend/src/lib/draftConflictSession.svelte.ts
T
Ruben FiszelandClaude Opus 5 d8ebbc06f5 fix: tell the user when a draft conflict has stopped their edits saving (#11241)
* fix: tell the user when a draft conflict has stopped their edits saving

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: give nothing up until the other version has actually loaded

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: ignore a reload the editor has moved on from, and settle saves before resolving

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: settle the key before a forced resolution, and cover quiesce with tests

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: let a resolution tell it belongs to an editor that is gone

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: end a conflict resolution when the editing session does, not the component

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: show the resource conflict in the fixed banner, and scope busy to its session

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the conflict alert in editors no host wraps

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: free a reopened editor from a resolution the closed one left in flight

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: make switching workspace-specific versions a new conflict session

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the local draft when a conflict resolution is abandoned

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: let a newer session outrank a resolution that outlived its editor

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: end a conflict resolution with the session that started it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: reopen a conflicted editor on the version the server refused

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep a refused draft deletion a deletion

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: ungate the editor when a resolution loads the other draft

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: settle the draft key before reading the version to load

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: settle the key again after reading, for edits made under the read

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 14:39:49 +00:00

66 lines
2.2 KiB
TypeScript

import { untrack } from 'svelte'
/**
* Identity for one editing session's conflict resolution.
*
* An editor outlives what it shows, so an awaited step cannot tell from the workspace and path
* alone whether it still speaks for what is on screen: both come back to the values it started
* with when the user returns to where they were. Every resolution takes a token and checks it
* before touching shared editor state.
*/
export function createDraftConflictSession() {
let generation = $state(0)
/** The token of the resolution in flight, or 0. A token rather than a flag: one left over from
* a closed session must not clear the busy state of the session that replaced it. */
let inFlight = $state(0)
return {
/** A resolution started in the current session is in flight. Ending a session clears it, so
* a reopened editor never shows buttons that only a stale request settling can re-enable. */
get busy(): boolean {
return inFlight !== 0
},
/** Claim the session for a resolution about to start. */
start(): number {
inFlight = ++generation
return inFlight
},
/** Whether `token` still speaks for the editor. */
holds(token: number): boolean {
return token === generation
},
/** Release `token`'s claim. A stale token is ignored. */
finish(token: number): void {
if (inFlight === token) inFlight = 0
},
/** Nothing outstanding speaks for this editor any more: a different item, a different
* session on the same one, or the component going away. */
end(): void {
generation++
inFlight = 0
}
}
}
/**
* A session for an editor that swaps what it shows in place, `scopeOf` naming what that currently
* is. Leaving a scope and coming back is a new session, so the change itself ends the old one:
* afterwards every value a resolution captured before the switch reads as current again, and no
* later comparison could tell it apart from one that never left.
*/
export function useDraftConflictSession(scopeOf: () => unknown) {
const session = createDraftConflictSession()
let lastScope = untrack(scopeOf)
$effect(() => {
const next = scopeOf()
untrack(() => {
if (next === lastScope) return
lastScope = next
session.end()
})
})
return session
}