diff --git a/frontend/src/lib/components/DiffDrawer.svelte b/frontend/src/lib/components/DiffDrawer.svelte index 963edba36f..ad3d83ef9a 100644 --- a/frontend/src/lib/components/DiffDrawer.svelte +++ b/frontend/src/lib/components/DiffDrawer.svelte @@ -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 { diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 4ddfc53847..d4e56f439d 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -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 diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index a915727282..a21daf45df 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -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 diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index ab043f81a1..a1855591b6 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -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: { diff --git a/frontend/src/lib/components/diff_drawer.ts b/frontend/src/lib/components/diff_drawer.ts index c7af1d4930..17972a4090 100644 --- a/frontend/src/lib/components/diff_drawer.ts +++ b/frontend/src/lib/components/diff_drawer.ts @@ -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 diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 43fa1abfe0..287ea9f809 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -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),