From 63fd19281bd732225ea06ee1b8de765e89947d98 Mon Sep 17 00:00:00 2001 From: Zach Leatherman Date: Thu, 3 Sep 2026 14:55:09 -0500 Subject: [PATCH] Fix for bfcache always reloading the page when back button is used --- client/reload-client.js | 22 +++++++++++++++++++--- server.js | 12 ++++++++++++ test/testServerRequests.js | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/client/reload-client.js b/client/reload-client.js index b863548..052344f 100644 --- a/client/reload-client.js +++ b/client/reload-client.js @@ -114,6 +114,7 @@ class ReloadClient { #socket; #ack = []; #ready = false; // swap to Promise.withResolvers + #buildId; // specific to one single core build run static RELOAD_ENABLED = true; static PORT_PARAM = "reloadPort"; @@ -266,6 +267,13 @@ class ReloadClient { this.reconnectInterval = ReloadClient.RECONNECT_INTERVAL; this.connectionMessageShown = false; this.reconnectEventCallback = this.reconnect.bind(this); + + // bfcache restores resume with a closed socket: reconnect now, don’t wait on the timer. + window.addEventListener("pageshow", (event) => { + if(event.persisted && this.#socket?.readyState !== WebSocket.OPEN) { + this.reconnect(event); + } + }); } get socket() { @@ -312,12 +320,18 @@ class ReloadClient { let e = JSON.parse(data.error); Util.error(`Build error: ${e.message}`, e); } else if (type === "eleventy.status") { - // Full page reload on initial reconnect + // A reconnect may have missed builds, but usually hasn’t—only reload if the + // server moved on. No `buildId` (older server) reloads unconditionally, as before. if (data.status === "connected" && options.mode === "reconnect") { - ReloadClient.reload({ via: "reconnect"}); + if(!this.#buildId || !data.buildId || this.#buildId !== data.buildId) { + ReloadClient.reload({ via: "reconnect"}); + } else { + Util.log(`Reconnected without page reload.`); + } } if(data.status === "connected") { + this.#buildId = data.buildId; // With multiple windows, only show one connection message if(!this.isConnected) { Util.log(Util.capitalize(data.status)); @@ -372,7 +386,9 @@ class ReloadClient { this.init({ mode: "reconnect" }); } - async onreload({ subtype, files, build }) { + async onreload({ subtype, files, build, buildId }) { + this.#buildId = buildId; + if(!ReloadClient.reloadTypes[subtype]) { subtype = "default"; } diff --git a/server.js b/server.js index 7beca08..894ce3a 100644 --- a/server.js +++ b/server.js @@ -136,6 +136,10 @@ export default class EleventyDevServer { #serverState; #readyPromise; #readyResolve; + // `buildId` names the current content: new per process, bumped per reload. Clients compare + // it on reconnect to see if they missed a build—see client/reload-client.js + #serverInstanceId = crypto.randomUUID(); + #buildCount = 0; static getServer(...args) { return new EleventyDevServer(...args); @@ -163,6 +167,10 @@ export default class EleventyDevServer { return this.options.logger; } + get buildId() { + return `${this.#serverInstanceId}:${this.#buildCount}`; + } + normalizeOptions(options = {}) { this.options = Object.assign({}, DEFAULT_OPTIONS, options); @@ -999,6 +1007,7 @@ export default class EleventyDevServer { this.sendUpdateNotification({ type: "eleventy.status", status: "connected", + buildId: this.buildId, }, { include: ws }); ws.on("message", (data) => { @@ -1223,11 +1232,14 @@ export default class EleventyDevServer { }); } + this.#buildCount++; + this.sendUpdateNotification({ type: "eleventy.reload", subtype, files, build, + buildId: this.buildId, }); } } diff --git a/test/testServerRequests.js b/test/testServerRequests.js index 798a0d2..355ad7a 100644 --- a/test/testServerRequests.js +++ b/test/testServerRequests.js @@ -489,4 +489,41 @@ test("Web Socket request", async (t) => { await server.close(); }); +test("Web Socket buildId, sent on connect and bumped by each reload", async (t) => { + let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); + server.serve(8201); + + let port = await server.getPort(); + + let socket = new WebSocket(`ws://localhost:${port}`); + let nextMessage = () => { + let { promise, resolve } = withResolvers(); + socket.addEventListener("message", (event) => resolve(JSON.parse(event.data)), { once: true }); + return promise; + }; + + let connected = await nextMessage(); + t.is(connected.status, "connected"); + // a client reconnecting with this id missed nothing + t.is(connected.buildId, server.buildId); + + let reloadedPromise = nextMessage(); + server.reload({ subtype: "css", files: [] }); + let reloaded = await reloadedPromise; + + t.is(reloaded.type, "eleventy.reload"); + t.is(reloaded.buildId, server.buildId); + t.not(reloaded.buildId, connected.buildId); + + await server.close(); +}); + +test("Web Socket buildId is unique per server instance", async (t) => { + let first = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); + let second = new EleventyDevServer("test-server", "./test/stubs/", getOptions()); + + // a restart never matches, so clients do reload + t.not(first.buildId, second.buildId); +}); + // TODO setup websocket server *without* web server \ No newline at end of file