fix: first-class dev hot-reload (no restart flash, watch outside appDir)#896
Conversation
|
Design: why keep The issue proposed making the in-process So I kept the restart and fixed the thing the user actually feels: the restart being VISIBLE. The two symptoms both come from the browser reloading at the wrong moment relative to the restart, not from the restart itself:
This is the same shape as how Vite's client survives a server restart. Net effect: same restart latency as before (unavoidable, the new code isn't ready until the process restarts), but the restart is now invisible, no flash, no manual refresh. If we ever want true instant module-level reload on Node, that's the |
vivek7405
left a comment
There was a problem hiding this comment.
Review: graceful-reload protocol, one real state-loss vector
Went through the reconnect state machine, the overlap/dedupe logic, the emitted client JS, and the docs/schema. The #894 side and the probe-then-reload gate are solid. One genuine problem on the #893 side: the reconnect-reload fired on ANY EventSource drop-then-reopen, not just a real restart, so a laptop sleep/wake or a network blip would silently full-reload the tab and throw away in-page dev state. Fixed by keying the reload on a per-process boot id carried on the hello frame, so only a real restart reloads.
vivek7405
left a comment
There was a problem hiding this comment.
Review: boot-id reconnect mechanism, clean
Went back over the boot-id path end to end: the comparison in the relay and the fallback (first hello baselines, same-process reconnect is a no-op, a changed id reloads), that DEV_BOOT_ID is one module-scope value shared by both listener shells and stable across the dev re-import, the hello frame format, and the tests. Nothing to change.
An app that renders content from OUTSIDE its appDir (the website builds blog posts from a repo-root blog/ dir, a sibling of the app) got no live-reload when that content changed: the dev watcher only follows the appDir recursively, so an edit to an outside file fired nothing and needed a manual refresh. Add an opt-in webjs.dev.watch array: extra directories the dev live-reload watcher follows alongside the appDir. Each entry resolves relative to the app root and may escape it (../blog); a change under one runs the same rebuild + browser reload as an in-tree edit. Missing paths are skipped, appDir overlaps (ancestor/descendant) are dropped as redundant, and the usual node_modules / .git / .webjs / db noise is ignored under each extra root. Wire the website's blog/ dir so its previews reload. Closes #894
…efresh On Node, webjs dev re-execs under node --watch, which restarts the whole process on an app edit. During the restart the server is briefly down, so the browser's live-reload could land on a still-restarting server and paint a style-less page (CSS gone, only HTML). And when node --watch killed the process before the in-process rebuild's reload frame went out, no reload fired at all, so the edit needed a manual refresh. Fix the client-side reload protocol (the approach Vite's client uses to survive a restart), keeping node --watch's full restart (it is what makes deep transitive-import edits take effect, since the dev ?t= cache-bust only refreshes the entry module): - Probe-then-reload: gate every reload on a /__webjs/version readiness check, so a reload never paints into a half-restarted server. Instant under an in-process reload; waits out a restart otherwise. - Reconnect-reload: a reconnect after a drop (a restart) is itself the edit signal, so an edit whose reload frame was killed with the old process still reloads with no manual refresh. Lives in the shared-connection relay and the per-tab fallback. - A short retry: 300 on the SSE hello shrinks the EventSource reconnect backoff from its ~3s default so that reconnect is prompt, on both listener shells. Closes #893
The reconnect-reload treated any EventSource drop-then-reopen as an edit signal, but EventSource fires error on every transient drop (laptop sleep/wake, a network blip, a tab evicted at the HTTP/1.1 connection cap). On such a reconnect the version probe passes immediately (the server never restarted) and the tab did a full reload anyway, discarding in-page dev state. Stamp the SSE hello frame with a per-process boot id (DEV_BOOT_ID, regenerated on every server start) and reload on reconnect only when that id CHANGES (a real node --watch restart). A transient reconnect hits the same process with the same id, so it never reloads. Replaces the open/error state machine in both the shared-connection relay and the per-tab fallback.
watch-extra-paths-live.test.js and reload-retry-hint.test.js boot startServer and read the port via node:http's server.address() shape, which the Bun.serve shell does not expose, so bun test failed them. Their Bun parity is proven by the dedicated test/bun/dev-extra-watch.mjs and dev-reload-retry.mjs scripts (added as their own CI steps); denylist the node-only integration versions, matching the existing dev-handler.test.js pattern.
7ebcedc to
4950173
Compare
Closes #893
Closes #894
Two dev-server hot-reload gaps found while previewing the website locally, fixed together.
What was wrong
#894 (watch outside the appDir). The dev watcher only follows the appDir recursively. An app that reads content from outside its tree (the website renders blog posts from a repo-root
blog/dir, a sibling of the app) got no reload when that content changed, so editing a post needed a manual browser refresh.#893 (restart flash + manual refresh on app edits). On Node,
webjs devre-execs undernode --watch, which restarts the whole process on an app edit. During the restart the server is briefly down, so the browser's live-reload could land on a still-restarting server and paint a style-less page ("CSS went off, only HTML appeared"). And whennode --watchkilled the process before the in-process rebuild's reload frame went out, no reload fired at all, so the edit needed a manual refresh.The fix
webjs.dev.watcharray of extra dirs the watcher follows alongside the appDir. Wired the website'sblog/./__webjs/versionbefore reloading (never paint into a down server), treat an EventSource reconnect-after-drop as the edit signal (so an edit whose reload frame was killed still reloads with no manual refresh), and a shortretry: 300on the SSE hello so that reconnect is prompt. Keeps Node'snode --watchfull restart, which is what makes deep transitive-import edits take effect (the dev?t=cache-bust only refreshes the entry module). Design detail in a comment below.Test plan
readDevWatchPathsFromApp(resolution, dedupe, overlap-skip, garbage-guard), 7 pass/__webjs/versionbefore reloading, base-path aware, plus the live SSEretry: 300hintdev-extra-watch.mjs(dogfood: watch content dirs the app reads from outside its appDir #894) anddev-reload-retry.mjs(dogfood: Node dev full-restarts on every app edit (downtime + CSS flash) #893) green on Node AND Bun 1.3.14reload-worker.test.jsrun happens in CI (local Playwright launch hangs in this sandbox)packages/server/test/devplustest/configtests passDocs
agent-docs/configuration.md(thewebjs.dev.watchblock), theWebjsConfigtype (packages/core/src/webjs-config.d.ts),packages/server/webjs-config.schema.json, andpackages/server/AGENTS.md(the dev.js reload mechanism).