diff --git a/packages/cli/src/commands/preview.ts b/packages/cli/src/commands/preview.ts index 62bedf8338..855c52369c 100644 --- a/packages/cli/src/commands/preview.ts +++ b/packages/cli/src/commands/preview.ts @@ -54,6 +54,7 @@ import { runRenderSetupWorker } from "../utils/cancellableProcess.js"; import { formatLintStartupMessage } from "../utils/lintFormat.js"; import { activeServerOnPort, + closeServer, findPortAndServe, scanActiveServers, killActiveServers, @@ -1743,7 +1744,7 @@ export async function runEmbeddedMode( .catch(() => {}) .finally(() => { watcher.close(); - result.server.close(() => resolveRun()); + void closeServer(result.server).then(resolveRun); }); }; // `on`, not `once`: a repeat Ctrl+C/SIGTERM while shutdown is running must diff --git a/packages/cli/src/server/portUtils.test.ts b/packages/cli/src/server/portUtils.test.ts index e21a9f6a2d..ac5562367f 100644 --- a/packages/cli/src/server/portUtils.test.ts +++ b/packages/cli/src/server/portUtils.test.ts @@ -2,10 +2,11 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createServer, type Server } from "node:net"; import { spawn } from "node:child_process"; import { resolve } from "node:path"; -import { createServer as createHttpServer, type Server as HttpServer } from "node:http"; +import { createServer as createHttpServer, get, type Server as HttpServer } from "node:http"; import { PORT_PROBE_HOSTS, activeServerOnPort, + closeServer, detectHyperframesServer, findPortAndServe, testPortOnAllHosts, @@ -306,3 +307,25 @@ describe("detectHyperframesServer", () => { expect(result).toEqual({ type: "match" }); }); }); + +describe("closeServer", () => { + it("closes while a client holds an event stream open", async () => { + const server = createHttpServer((_req, res) => { + res.writeHead(200, { "content-type": "text/event-stream" }); + res.write(": open\n\n"); + }); + const port = await new Promise((resolve) => + server.listen(0, "127.0.0.1", () => + resolve((server.address() as import("node:net").AddressInfo).port), + ), + ); + await new Promise((resolve) => + get(`http://127.0.0.1:${port}/`, (res) => { + res.on("error", () => {}); + resolve(); + }).on("error", () => {}), + ); + await closeServer(server); + expect(server.listening).toBe(false); + }); +}); diff --git a/packages/cli/src/server/portUtils.ts b/packages/cli/src/server/portUtils.ts index c0a1971118..3c13d1967f 100644 --- a/packages/cli/src/server/portUtils.ts +++ b/packages/cli/src/server/portUtils.ts @@ -413,6 +413,12 @@ export async function killActiveServers( return { killed, unverified }; } +/** Stops `server` once every open connection is closed too; an open Studio tab holds its event stream for its life. */ +export function closeServer(server: import("@hono/node-server").ServerType): Promise { + if ("closeAllConnections" in server) server.closeAllConnections(); + return new Promise((resolve) => server.close(() => resolve())); +} + // ── Smart port selection ─────────────────────────────────────────────────── export type FindPortResult = diff --git a/packages/cli/src/server/studioServer.test.ts b/packages/cli/src/server/studioServer.test.ts index 1cb82924b3..962eef661d 100644 --- a/packages/cli/src/server/studioServer.test.ts +++ b/packages/cli/src/server/studioServer.test.ts @@ -891,6 +891,17 @@ describe("Studio file-change SSE", () => { return { projectDir, projectUrl }; } + it("leaves no timer behind once the tab disconnects, so a stopping CLI can exit", async () => { + server = createStudioServer({ projectDir: tmpProject() }); + const timeouts = vi.spyOn(globalThis, "setTimeout"); + const [stream] = await subscribe(1); + await stream!.cancel(); + + const keepAlive = timeouts.mock.calls.filter(([, ms]) => (ms ?? 0) >= 30_000); + timeouts.mockRestore(); + expect(keepAlive).toEqual([]); + }); + it("marks a notes write as not affecting the preview, so the tab does not reload", async () => { const { projectDir } = await previewedProject(); const [stream] = await subscribe(1); diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index 52c60eaaa3..91bba7cfa8 100644 --- a/packages/cli/src/server/studioServer.ts +++ b/packages/cli/src/server/studioServer.ts @@ -1034,14 +1034,8 @@ export function createStudioServer(options: StudioServerOptions): StudioServer { if (shouldWatchProjectFile(changedPath)) listener(changedPath); }; watcher.addListener(wrappedListener); - stream.onAbort(() => watcher.removeListener(wrappedListener)); - try { - while (true) { - await stream.sleep(30000); - } - } finally { - watcher.removeListener(wrappedListener); - } + if (!stream.aborted) await new Promise((tabClosed) => stream.onAbort(tabClosed)); + watcher.removeListener(wrappedListener); }); });