fix: taking the diff drawer without a token claims it, and the classic app editor takes one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-15 12:13:37 +02:00
co-authored by Claude Opus 5
parent f71262dc7f
commit 7b490b87a8
6 changed files with 43 additions and 17 deletions
+10 -4
View File
@@ -68,7 +68,13 @@
}
| undefined = $state(undefined)
export function openDrawer() {
export function openDrawer(token?: number) {
// No token means the caller is taking the drawer for itself, so claim one here:
// every reuse (a deploy-override's "Show diff", a draft badge, a workspace
// comparison) then invalidates an editor opening that is still fetching, instead
// of being replaced by it when it lands.
if (token != null && token !== openingToken) return
if (token == null) openingToken++
data = undefined
diffType = undefined
diffViewer?.openDrawer()
@@ -80,9 +86,9 @@
/** Counted per opening, and counted here rather than in the editor that opens one: a
* path change remounts the editor while this drawer stays mounted, so a counter local
* to the editor is one an outlived request still matches it would open and fill the
* drawer with the item the user just left. Every write an opening makes (the blanking
* `openDrawer` included) checks `ownsOpening` first. */
* to the editor is one an outlived request still matches, and it would open and fill
* the drawer with the item the user just left. Every write an opening makes (the
* blanking `openDrawer` included) checks `ownsOpening` first. */
let openingToken = 0
export function beginOpening(): number {
@@ -1181,7 +1181,7 @@
const currentDraftTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
// Blanking the drawer belongs to the opening that will fill it.
if (!diffDrawer?.ownsOpening(opening)) return
diffDrawer.openDrawer()
diffDrawer.openDrawer(opening)
const currentFlow = flowStore.val
const versions = await deployedVersionOptions()
if (!diffDrawer?.ownsOpening(opening)) return
@@ -905,7 +905,7 @@
// Blanking the drawer belongs to the opening that will fill it.
if (!diffDrawer?.ownsOpening(opening)) return
diffDrawer.openDrawer()
diffDrawer.openDrawer(opening)
const headHash = (deployed as { hash?: string } | undefined)?.hash
const versions = await deployedVersionOptions(headHash)
if (!diffDrawer?.ownsOpening(opening)) return
@@ -32,7 +32,7 @@
Zap,
Globe
} from 'lucide-svelte'
import { getContext, untrack } from 'svelte'
import { getContext, onDestroy, untrack } from 'svelte'
import { orderedJsonStringify, type Value, replaceFalseWithUndefined } from '../../../utils'
import type { App, AppEditorContext, AppViewerContext } from '../types'
import { toStatic } from '../utils'
@@ -329,13 +329,20 @@
}
}
async function syncWithDeployed() {
// An opening outlives this editor when a path change remounts it mid-fetch; without
// this it would still open the drawer on the app the user left.
onDestroy(() => diffDrawer?.abandonOpening())
async function syncWithDeployed(opening?: number) {
const deployedApp = await AppService.getAppByPath({
workspace: $workspaceStore!,
path: $appPath!,
withStarredInfo: true
})
// A superseded opening must not write these: the current one would then render
// against the older deployed value.
if (opening != null && !diffDrawer?.ownsOpening(opening)) return
deployedBy = deployedApp.created_by
// Strip off extra information
@@ -624,12 +631,18 @@
if (!savedApp || newApp) {
return
}
// The fetch below is awaited, so a reopen (or a path change, which remounts
// this editor but not the drawer) while it runs must not have the older one
// land last. The drawer counts the openings for that reason.
const opening = diffDrawer?.beginOpening()
if (opening == null) return
// deployedValue should be syncronized when we open Diff
await syncWithDeployed()
await syncWithDeployed(opening)
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
if (!diffDrawer?.ownsOpening(opening)) return
diffDrawer.openDrawer(opening)
diffDrawer.setDiff({
mode: 'normal',
deployed: deployedValue ?? savedApp,
current: {
@@ -759,12 +772,16 @@
if (!savedApp || newApp) {
return
}
// The other entry point into the same drawer, so it takes an opening too.
const opening = diffDrawer?.beginOpening()
if (opening == null) return
// deployedValue should be syncronized when we open Diff
await syncWithDeployed()
await syncWithDeployed(opening)
if (!diffDrawer?.ownsOpening(opening)) return
saveDrawerOpen = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
diffDrawer.openDrawer(opening)
diffDrawer.setDiff({
mode: 'normal',
deployed: deployedValue ?? savedApp,
current: {
+4 -1
View File
@@ -53,7 +53,10 @@ export type DiffDrawerDiff =
}
export interface DiffDrawerI {
openDrawer: () => void
/** Pass the token from `beginOpening` to continue that opening; called without one,
* the drawer claims a fresh opening, so any reuse invalidates a fetch still in
* flight rather than being overwritten by it. */
openDrawer: (token?: number) => void
closeDrawer: () => void
setDiff: (diff: DiffDrawerDiff) => void
/** Claim the drawer for one opening. Filling it takes awaited fetches, and a path
@@ -472,7 +472,7 @@
// Blanking the drawer belongs to the opening that will fill it.
if (!diffDrawer?.ownsOpening(opening)) return
diffDrawer.openDrawer()
diffDrawer.openDrawer(opening)
const versions = await deployedVersionOptions()
if (!diffDrawer?.ownsOpening(opening)) return
diffDrawer.setDiff({
@@ -704,7 +704,7 @@
if (!diffDrawer?.ownsOpening(opening)) return
saveDrawerOpen = false
diffDrawer.openDrawer()
diffDrawer.openDrawer(opening)
diffDrawer.setDiff({
mode: 'normal',
deployed: deployedValue ?? stripRawAppDiffNoise(savedApp),