mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: let a resolution tell it belongs to an editor that is gone
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0b3fa66b64
commit
f6c4ebbfb2
@@ -265,6 +265,14 @@
|
||||
/** A resolution is in flight. Both buttons go disabled: clicking the other one midway would
|
||||
* race two resolutions of one conflict against each other. */
|
||||
let resolvingConflict = $state(false)
|
||||
/** Identifies the editor instance and the resolution within it. Comparing `selected`/path
|
||||
* alone is not enough: closing and reopening the same item gives a *new* editor those same
|
||||
* values, so a resolution left over from the old one would pass that check and write into it.
|
||||
* Bumped per resolution and zeroed on teardown, so a stale one can always tell it is stale. */
|
||||
let resolveGeneration = 0
|
||||
onDestroy(() => {
|
||||
resolveGeneration = -1
|
||||
})
|
||||
/** The server refused this tab's autosave because the row moved under it: another tab, or the
|
||||
* AI chat, which writes these drafts too. Nothing typed here reaches the server until the user
|
||||
* picks a version, and the unsaved-changes banner says the opposite — that the edits are held
|
||||
@@ -282,8 +290,10 @@
|
||||
async function resolveDraftConflict(keepMine: boolean): Promise<void> {
|
||||
const ws = selected
|
||||
const p = initialPath
|
||||
if (!ws || !p || resolvingConflict) return
|
||||
if (!ws || !p || resolvingConflict || resolveGeneration < 0) return
|
||||
const query = { workspace: ws, itemKind: 'resource' as const, path: p }
|
||||
const gen = ++resolveGeneration
|
||||
const stillOurs = () => gen === resolveGeneration && selected === ws && initialPath === p
|
||||
resolvingConflict = true
|
||||
try {
|
||||
if (keepMine) {
|
||||
@@ -291,7 +301,7 @@
|
||||
// write below, and being conditional it would be refused — so "Keep mine" would
|
||||
// finish without keeping anything and leave the alert standing.
|
||||
await UserDraftDbSyncer.quiesce(query)
|
||||
if (selected !== ws || initialPath !== p) return
|
||||
if (!stillOurs()) return
|
||||
// Forced, so it goes over the row that refused us, and its response reseeds
|
||||
// `last_sync` so the next ordinary save is conditional again.
|
||||
const mine = states[ws]?.draft
|
||||
@@ -317,13 +327,13 @@
|
||||
// resource's: the drawer stays closable while the read is out, and another resource
|
||||
// opened meanwhile would otherwise get this one's baseline — and with it this one's
|
||||
// path as its save target.
|
||||
if (selected !== ws || initialPath !== p) return
|
||||
if (!stillOurs()) return
|
||||
// Anything an autosave queued while the read was out belongs to the version being
|
||||
// replaced. Dropping is not enough on its own: a POST the runner already started
|
||||
// cannot be cancelled, and if it settles after the baseline below, its rejection
|
||||
// raises the conflict again. So wait for the chain to go quiet first.
|
||||
await UserDraftDbSyncer.quiesce(query)
|
||||
if (selected !== ws || initialPath !== p) return
|
||||
if (!stillOurs()) return
|
||||
UserDraftDbSyncer.clearConflict(query)
|
||||
initialStates[ws] = structuredClone(deployedState)
|
||||
UserDraftDbSyncer.recordRemoteSync(query, (r as any).draft_saved_at)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { VariableService, WorkspaceService } from '$lib/gen'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { createEventDispatcher, onDestroy, untrack } from 'svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Button } from './common'
|
||||
import Drawer from './common/drawer/Drawer.svelte'
|
||||
@@ -128,6 +128,14 @@
|
||||
/** A resolution is in flight. Both buttons go disabled: clicking the other one midway would
|
||||
* race two resolutions of one conflict against each other. */
|
||||
let resolvingConflict = $state(false)
|
||||
/** Identifies the editor instance and the resolution within it. Comparing `selected`/path
|
||||
* alone is not enough: closing and reopening the same item gives a *new* editor those same
|
||||
* values, so a resolution left over from the old one would pass that check and write into it.
|
||||
* Bumped per resolution and zeroed on teardown, so a stale one can always tell it is stale. */
|
||||
let resolveGeneration = 0
|
||||
onDestroy(() => {
|
||||
resolveGeneration = -1
|
||||
})
|
||||
/** The server refused this tab's autosave because the row moved under it: another tab, or the
|
||||
* AI chat, which writes these drafts too. Nothing typed here reaches the server until the user
|
||||
* picks a version, and the unsaved-changes banner says the opposite — that the edits are held
|
||||
@@ -145,8 +153,10 @@
|
||||
async function resolveDraftConflict(keepMine: boolean): Promise<void> {
|
||||
const ws = selected
|
||||
const p = editPath
|
||||
if (!ws || !p || resolvingConflict) return
|
||||
if (!ws || !p || resolvingConflict || resolveGeneration < 0) return
|
||||
const query = { workspace: ws, itemKind: 'variable' as const, path: p }
|
||||
const gen = ++resolveGeneration
|
||||
const stillOurs = () => gen === resolveGeneration && selected === ws && editPath === p
|
||||
resolvingConflict = true
|
||||
try {
|
||||
if (keepMine) {
|
||||
@@ -154,7 +164,7 @@
|
||||
// write below, and being conditional it would be refused — so "Keep mine" would
|
||||
// finish without keeping anything and leave the alert standing.
|
||||
await UserDraftDbSyncer.quiesce(query)
|
||||
if (selected !== ws || editPath !== p) return
|
||||
if (!stillOurs()) return
|
||||
// Forced, so it goes over the row that refused us, and its response reseeds
|
||||
// `last_sync` so the next ordinary save is conditional again.
|
||||
const mine = states[ws]?.draft
|
||||
@@ -188,13 +198,13 @@
|
||||
// variable's: the drawer stays closable while the read is out, and another variable
|
||||
// opened meanwhile would otherwise get this one's baseline — and with it this one's
|
||||
// path as its save target.
|
||||
if (selected !== ws || editPath !== p) return
|
||||
if (!stillOurs()) return
|
||||
// Anything an autosave queued while the read was out belongs to the version being
|
||||
// replaced. Dropping is not enough on its own: a POST the runner already started
|
||||
// cannot be cancelled, and if it settles after the baseline below, its rejection
|
||||
// raises the conflict again. So wait for the chain to go quiet first.
|
||||
await UserDraftDbSyncer.quiesce(query)
|
||||
if (selected !== ws || editPath !== p) return
|
||||
if (!stillOurs()) return
|
||||
UserDraftDbSyncer.clearConflict(query)
|
||||
initialStates[ws] = structuredClone(deployedState)
|
||||
UserDraftDbSyncer.recordRemoteSync(query, (v as any).draft_saved_at)
|
||||
|
||||
Reference in New Issue
Block a user