From cdd14b17ac7cb35e17180a5988406f1eca15ab95 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 13 Jul 2026 23:05:43 -0500 Subject: [PATCH 1/2] feat(client): add disconnect() to close the SSE connection Expose a way to close the EventSource for the current path and stop the reconnection watchdog (e.g. before tearing the page down). The cached wrapper is dropped so a later setOptionsAndConnect() opens a fresh connection. Ref webpack/webpack-hot-middleware#367 --- README.md | 4 ++++ client-src/index.js | 25 ++++++++++++++++++++++++- test/client.test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 78df93da3..c934566d9 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,10 @@ hotClient.useCustomOverlay({ // Connect manually when `autoConnect=false`. Accepts the same option keys as // the query-string API above. hotClient.setOptionsAndConnect({ path: "/__hmr" }); + +// Close the SSE connection and stop reconnecting (e.g. before tearing the +// page down). A later `setOptionsAndConnect` call opens a fresh connection. +hotClient.disconnect(); ``` ## API diff --git a/client-src/index.js b/client-src/index.js index c71d370ae..f975f62c9 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -104,9 +104,17 @@ function createEventSourceWrapper() { } }; - const handleDisconnect = () => { + /** + * Close the connection and stop the activity timer without scheduling a + * reconnection. + */ + const close = () => { clearInterval(timer); source.close(); + }; + + const handleDisconnect = () => { + close(); setTimeout(init, /** @type {number} */ (options.timeout)); }; @@ -134,6 +142,7 @@ function createEventSourceWrapper() { addMessageListener(fn) { listeners.push(fn); }, + close, }; } @@ -179,6 +188,20 @@ export function setOptionsAndConnect(overrides) { connect(); } +/** + * Close the SSE connection for the current path and stop reconnecting. A + * later `setOptionsAndConnect` call opens a fresh connection. + */ +export function disconnect() { + const path = /** @type {string} */ (options.path); + const wrappers = window[WRAPPER_KEY]; + + if (wrappers && wrappers[path]) { + wrappers[path].close(); + delete wrappers[path]; + } +} + // eslint-disable-next-line jsdoc/reject-any-type /** @typedef {any} EXPECTED_ANY */ diff --git a/test/client.test.js b/test/client.test.js index 37cd09e26..1a929561e 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -597,6 +597,32 @@ describe("client", () => { jest.advanceTimersByTime(20 * 1000); expect(EventSourceStub.instances).toHaveLength(2); }); + + it("disconnect() closes the connection and does not reconnect", () => { + jest.useFakeTimers({ doNotFake: ["nextTick"] }); + delete globalThis.__wdmEventSourceWrapper; + EventSourceStub.instances.length = 0; + client = loadClient(); + + const [first] = EventSourceStub.instances; + client.disconnect(); + + expect(first.closed).toBe(true); + // Neither the watchdog nor a reconnect timer may re-open it. + jest.advanceTimersByTime(60 * 1000); + expect(EventSourceStub.instances).toHaveLength(1); + }); + + it("disconnect() drops the cached wrapper so a new connect starts fresh", () => { + client.disconnect(); + expect( + globalThis.__wdmEventSourceWrapper["/__webpack_hmr"], + ).toBeUndefined(); + + client.setOptionsAndConnect({}); + expect(EventSourceStub.instances).toHaveLength(2); + expect(EventSourceStub.lastInstance().closed).toBe(false); + }); }); describe("with logging option", () => { From ac04a80b0c836ad0b057d0d782558606627bf389 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 20 Jul 2026 15:03:40 -0500 Subject: [PATCH 2/2] fixup! --- client-src/globals.d.ts | 1 + client-src/index.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client-src/globals.d.ts b/client-src/globals.d.ts index 7f66bdd08..3c4550a3f 100644 --- a/client-src/globals.d.ts +++ b/client-src/globals.d.ts @@ -20,6 +20,7 @@ interface ClientReporter { interface EventSourceWrapper { addMessageListener(fn: (event: { data: string }) => void): void; + close(): void; } interface Window { diff --git a/client-src/index.js b/client-src/index.js index f975f62c9..0de0810d1 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -78,7 +78,7 @@ function setOverrides(overrides) { */ /** - * @returns {{ addMessageListener: (fn: MessageListener) => void }} event source wrapper + * @returns {{ addMessageListener: (fn: MessageListener) => void, close: () => void }} event source wrapper */ function createEventSourceWrapper() { /** @type {EventSource} */