From 2dec554abfbd2c57e936d5c54d9487fde69e8878 Mon Sep 17 00:00:00 2001 From: tristantr Date: Tue, 26 May 2026 14:52:51 +0200 Subject: [PATCH] fix(deploy-to-hub): paginate workspace list and cancel stale record polls - loadWorkspace fetches all pages instead of capping at 100 items per kind - pollJobUntilComplete now bails when recordRunSeq advances (new record target, re-run, or drawer close), preventing late completion of a previous run from overwriting current state Co-Authored-By: Claude Opus 4.7 (1M context) --- .../workspaceSettings/DeployToHub.svelte | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/components/workspaceSettings/DeployToHub.svelte b/frontend/src/lib/components/workspaceSettings/DeployToHub.svelte index f11582b969..072f5fb917 100644 --- a/frontend/src/lib/components/workspaceSettings/DeployToHub.svelte +++ b/frontend/src/lib/components/workspaceSettings/DeployToHub.svelte @@ -135,6 +135,7 @@ let runState = $state('idle') let runJobId = $state(undefined) let runResult = $state(undefined) + let recordRunSeq = 0 let runError = $state(undefined) // MOCK STORAGE: backend has no recordings table yet; we keep the job_id per item locally. let recordings = $state>({}) @@ -157,15 +158,28 @@ draftItems = draftItems.map((i) => (i.key === key ? { ...i, ...patch } : i)) } + async function listAllPages( + fetcher: (params: { perPage: number; page: number }) => Promise + ): Promise { + const perPage = 100 + const out: T[] = [] + for (let page = 1; page <= 1000; page++) { + const batch = await fetcher({ perPage, page }) + out.push(...batch) + if (batch.length < perPage) return out + } + return out + } + async function loadWorkspace(workspace: string) { loading = true try { const [apps, rawApps, flows, scripts, resources, settings] = await Promise.all([ - AppService.listApps({ workspace, perPage: 100 }), - RawAppService.listRawApps({ workspace, perPage: 100 }), - FlowService.listFlows({ workspace, perPage: 100 }), - ScriptService.listScripts({ workspace, perPage: 100 }), - ResourceService.listResource({ workspace, perPage: 100 }), + listAllPages((p) => AppService.listApps({ workspace, ...p })), + listAllPages((p) => RawAppService.listRawApps({ workspace, ...p })), + listAllPages((p) => FlowService.listFlows({ workspace, ...p })), + listAllPages((p) => ScriptService.listScripts({ workspace, ...p })), + listAllPages((p) => ResourceService.listResource({ workspace, ...p })), WorkspaceService.getSettings({ workspace }).catch(() => undefined) ]) @@ -339,6 +353,7 @@ } async function openRecord(it: DeployItem) { + recordRunSeq++ recordTarget = it recordArgs = {} recordValid = true @@ -372,6 +387,7 @@ const it = recordTarget const workspace = $workspaceStore if (!it || !workspace) return + const seq = ++recordRunSeq runState = 'running' runJobId = undefined runResult = undefined @@ -391,24 +407,28 @@ requestBody: recordArgs }) } else { - runState = 'idle' + if (seq === recordRunSeq) runState = 'idle' return } + if (seq !== recordRunSeq) return runJobId = jobId - await pollJobUntilComplete(workspace, jobId) + await pollJobUntilComplete(workspace, jobId, seq) } catch (e: any) { + if (seq !== recordRunSeq) return runState = 'failed' runError = `Failed to start: ${e?.message ?? e}` } } - async function pollJobUntilComplete(workspace: string, jobId: string) { + async function pollJobUntilComplete(workspace: string, jobId: string, seq: number) { for (let i = 0; i < 300; i++) { await delay(1000) + if (seq !== recordRunSeq) return try { const r = await JobService.getCompletedJobResultMaybe({ workspace, id: jobId }) + if (seq !== recordRunSeq) return if (r.completed) { runResult = r.result if (r.success) { @@ -420,11 +440,13 @@ return } } catch (e: any) { + if (seq !== recordRunSeq) return runState = 'failed' runError = `Polling failed: ${e?.message ?? e}` return } } + if (seq !== recordRunSeq) return runState = 'failed' runError = 'Timed out after 5 minutes' } @@ -782,7 +804,7 @@ - + recordRunSeq++}> recordDrawer?.closeDrawer()}