web: subscribe console on route changes

This commit is contained in:
2026-08-02 03:43:53 +09:00
parent e5f3c20f64
commit 606cd5fa31
3 changed files with 38 additions and 18 deletions
@@ -560,10 +560,11 @@ Deno.test("Worker Console page is routed by runtime_id and worker_id through bac
"reload token advancement should not synchronously read and write the rune state", "reload token advancement should not synchronously read and write the rune state",
); );
assert( assert(
consolePage.includes("advanceReloadToken();") && consolePage.includes("const token = advanceReloadToken();") &&
consolePage.includes("void loadConsoleData(target);") && consolePage.includes("worker = targetWorker;") &&
consolePage.includes("if (!targetWorker) void loadWorker(target, token);") &&
!consolePage.includes("void refreshConsole();\n });\n\n $effect"), !consolePage.includes("void refreshConsole();\n });\n\n $effect"),
"target-change effect should load data without depending on manual refresh state reads", "target-change effect should install route data and guard fallback loading with the new target token",
); );
assert( assert(
consolePage.includes( consolePage.includes(
@@ -725,10 +726,18 @@ Deno.test("Workspace Worker list and Console share the multiplexed connection",
new URL("./../multiplexer.ts", import.meta.url), new URL("./../multiplexer.ts", import.meta.url),
); );
assert( assert(
consolePage.includes("workspaceMultiplexer(workspaceId)") && consolePage.includes("workspaceMultiplexer(target.workspaceId)") &&
sidebarStore.includes("workspaceMultiplexer(workspaceId)") && sidebarStore.includes("workspaceMultiplexer(workspaceId)") &&
multiplexer.includes("const multiplexers = new Map") && multiplexer.includes("const multiplexers = new Map") &&
multiplexer.includes("frame: 'worker_protocol'"), multiplexer.includes("frame: 'worker_protocol'"),
"Sidebar and Console should share one Workspace multiplexer and route Worker methods through a subscription lane", "Sidebar and Console should share one Workspace multiplexer and route Worker methods through a subscription lane",
); );
assert(
multiplexer.includes("this.#socket?.readyState === WebSocket.OPEN") &&
multiplexer.includes("this.#sendSubscribe(subscription)") &&
consolePage.includes("const targetWorker = data.worker") &&
consolePage.includes("worker = targetWorker") &&
consolePage.includes("const consoleTarget = $derived({ workspaceId, runtimeId, workerId })"),
"A reused Console route should subscribe immediately on the live Workspace socket and install the new route Worker",
);
}); });
@@ -54,15 +54,21 @@ export class WorkspaceMultiplexer {
listener: Listener, listener: Listener,
): WorkspaceMultiplexerSubscription { ): WorkspaceMultiplexerSubscription {
const clientId = crypto.randomUUID(); const clientId = crypto.randomUUID();
this.#subscriptions.set(clientId, { const subscription: ActiveSubscription = {
clientId, clientId,
selector, selector,
listener, listener,
requestId: null, requestId: null,
subscriptionId: null, subscriptionId: null,
}); };
this.#subscriptions.set(clientId, subscription);
this.#closed = false; this.#closed = false;
if (this.#socket?.readyState === WebSocket.OPEN) {
subscription.listener.onStatus?.('open');
this.#sendSubscribe(subscription);
} else {
this.#ensureConnected(); this.#ensureConnected();
}
return { return {
close: () => this.#remove(clientId), close: () => this.#remove(clientId),
sendWorkerMethod: (method) => this.#sendWorkerMethod(clientId, method), sendWorkerMethod: (method) => this.#sendWorkerMethod(clientId, method),
@@ -140,11 +140,12 @@
let reloadToken = $state(0); let reloadToken = $state(0);
type ConsoleTarget = { type ConsoleTarget = {
workspaceId: string;
runtimeId: string; runtimeId: string;
workerId: string; workerId: string;
}; };
const consoleTarget = $derived({ runtimeId, workerId }); const consoleTarget = $derived({ workspaceId, runtimeId, workerId });
const lines = $derived(consoleProjection.lines); const lines = $derived(consoleProjection.lines);
const timelineLayout = $derived( const timelineLayout = $derived(
@@ -179,17 +180,20 @@
return response.json() as Promise<T>; return response.json() as Promise<T>;
} }
async function loadWorker(target: ConsoleTarget) { async function loadWorker(target: ConsoleTarget, token: number) {
workerError = null; workerError = null;
try { try {
const payload = await getJson<Worker>( const payload = await getJson<Worker>(
workerApiPath( workspaceApiPath(
target.workspaceId,
`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}`, `/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}`,
), ),
); );
if (token !== reloadToken) return;
worker = payload; worker = payload;
liveWorkerState = payload.state; liveWorkerState = payload.state;
} catch (error) { } catch (error) {
if (token !== reloadToken) return;
workerError = workerError =
error instanceof Error ? error.message : String(error); error instanceof Error ? error.message : String(error);
worker = null; worker = null;
@@ -197,10 +201,6 @@
} }
} }
async function loadConsoleData(target: ConsoleTarget) {
if (!worker) await loadWorker(target);
}
function advanceReloadToken(): number { function advanceReloadToken(): number {
nextReloadToken += 1; nextReloadToken += 1;
reloadToken = nextReloadToken; reloadToken = nextReloadToken;
@@ -515,7 +515,7 @@
return; return;
} }
protocolState = "connecting"; protocolState = "connecting";
const subscription = workspaceMultiplexer(workspaceId).subscribe( const subscription = workspaceMultiplexer(target.workspaceId).subscribe(
{ {
topic: "worker_protocol", topic: "worker_protocol",
worker_id: target.workerId, worker_id: target.workerId,
@@ -1083,11 +1083,16 @@
$effect(() => { $effect(() => {
const target = consoleTarget; const target = consoleTarget;
const targetWorker = data.worker;
const targetWorkerError = data.workerError;
resetObservedEvents(); resetObservedEvents();
liveWorkerState = null; worker = targetWorker;
workerError = targetWorkerError;
liveWorkerState = targetWorker?.state ?? null;
streamDiagnostics = []; streamDiagnostics = [];
advanceReloadToken(); protocolState = "connecting";
void loadConsoleData(target); const token = advanceReloadToken();
if (!targetWorker) void loadWorker(target, token);
}); });
$effect(() => connectProtocolTransport(worker, reloadToken, consoleTarget)); $effect(() => connectProtocolTransport(worker, reloadToken, consoleTarget));