diff --git a/web/workspace/src/lib/workspace-console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace-console/worker-console.ui.test.ts index d74dd520..20d2e14d 100644 --- a/web/workspace/src/lib/workspace-console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace-console/worker-console.ui.test.ts @@ -57,7 +57,7 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac ); assert( consolePage.includes( - "/api/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(workerId)}", + "/api/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}", ), "Worker detail should use the backend Worker detail API", ); @@ -75,4 +75,17 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac consolePage.includes("Streaming observation is not available"), "Console should show an explicit non-streaming degradation path", ); + assert( + consolePage.includes("function advanceReloadToken()") && + consolePage.includes("nextReloadToken += 1") && + !consolePage.includes("reloadToken += 1"), + "reload token advancement should not synchronously read and write the rune state", + ); + assert( + consolePage.includes( + "advanceReloadToken();\n void loadConsoleData(target);", + ) && + !consolePage.includes("void refreshConsole();\n });\n\n $effect"), + "target-change effect should load data without depending on manual refresh state reads", + ); }); diff --git a/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte b/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte index 02c611c8..6fb7fda6 100644 --- a/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte +++ b/web/workspace/src/routes/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte @@ -39,7 +39,15 @@ let streamState = $state<'connecting' | 'open' | 'unsupported' | 'closed' | 'error'>('connecting'); let streamDiagnostics = $state([]); let observedEvents = $state>([]); - let reloadToken = 0; + let nextReloadToken = 0; + let reloadToken = $state(0); + + type ConsoleTarget = { + runtimeId: string; + workerId: string; + }; + + const consoleTarget = $derived({ runtimeId, workerId }); const projection = $derived( projectConsole( @@ -101,11 +109,11 @@ } } - async function loadWorker() { + async function loadWorker(target: ConsoleTarget) { workerError = null; try { worker = await getJson( - `/api/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(workerId)}` + `/api/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}` ); } catch (error) { workerError = error instanceof Error ? error.message : String(error); @@ -113,11 +121,11 @@ } } - async function loadTranscript() { + async function loadTranscript(target: ConsoleTarget) { transcriptError = null; try { transcript = await getJson( - `/api/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent(workerId)}/transcript?limit=200` + `/api/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}/transcript?limit=200` ); } catch (error) { transcriptError = error instanceof Error ? error.message : String(error); @@ -125,9 +133,19 @@ } } + async function loadConsoleData(target: ConsoleTarget) { + await Promise.all([loadWorker(target), loadTranscript(target)]); + } + + function advanceReloadToken(): number { + nextReloadToken += 1; + reloadToken = nextReloadToken; + return nextReloadToken; + } + async function refreshConsole() { - reloadToken += 1; - await Promise.all([loadWorker(), loadTranscript()]); + advanceReloadToken(); + await loadConsoleData(consoleTarget); } async function sendMessage(event: SubmitEvent) { @@ -149,7 +167,7 @@ } else { sendError = diagnosticsToText(result.diagnostics) || `Input was ${result.state}.`; } - await loadTranscript(); + await loadTranscript(consoleTarget); } catch (error) { sendError = error instanceof Error ? error.message : String(error); } finally { @@ -157,12 +175,12 @@ } } - function connectObservation(target: Worker | null, token: number) { - if (!target) { + function connectObservation(targetWorker: Worker | null, token: number, target: ConsoleTarget) { + if (!targetWorker) { streamState = 'closed'; return; } - if (!target.capabilities.can_stream_events) { + if (!targetWorker.capabilities.can_stream_events) { streamState = 'unsupported'; streamDiagnostics = [ { @@ -177,8 +195,8 @@ streamState = 'connecting'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket( - `${protocol}//${window.location.host}/api/runtimes/${encodeURIComponent(runtimeId)}/workers/${encodeURIComponent( - workerId + `${protocol}//${window.location.host}/api/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent( + target.workerId )}/events/ws` ); @@ -261,12 +279,14 @@ }); $effect(() => { + const target = consoleTarget; observedEvents = []; streamDiagnostics = []; - void refreshConsole(); + advanceReloadToken(); + void loadConsoleData(target); }); - $effect(() => connectObservation(worker, reloadToken)); + $effect(() => connectObservation(worker, reloadToken, consoleTarget));