Skip to content
Snippets Groups Projects
Unverified Commit 682215fd authored by Braden Shepherdson's avatar Braden Shepherdson Committed by GitHub
Browse files

Hack to fix webpack serving stale CLJS on browser refresh (#34381)

### Background

CLJS output is not watched by webpack. (Watching it causes full page reloads I can't find a way to stop.)
CLJS changes are hot reloaded by shadow-cljs separate from webpack.

Changing CLJS code works great: shadow-cljs hot reloads it and it works nicely.

Changing JS code also works: webpack recompiles (including the latest CLJS output), the browser refreshes, and all is
well.

### The Problem

If you refresh the browser after you've changed only CLJS code, webpack does not know it must recompile the bundles.
Therefore it serves up the old bundles, which have now-stale CLJS output in them.

### The "Solution"

The first time shadow-cljs hot reloads after a CLJS change, a hook in `metabase.util.devtools` registers a
`beforeunload` handler. This handler sends a request to `http://localhost:8080/webpack-dev-server/invalidate`, forcing
webpack to recompile the bundles.

That isn't such a terrible hack by itself. The ugly part is that there's a race
condition between webpack invalidating the bundles and the browser trying to
load the bundles after the refresh, and sometimes the browser wins (loading the
stale CLJS). I added a spin-lock, repeatedly checking `performance.now()` until
500ms have passed, then the handler returns. This small delay gives webpack
time to mark the bundles invalid and allow the refresh.

I haven't experimented with how long that delay needs to be for this to be
reliable. Since this handler is only attached after CLJS code has been hot
reloaded, it will never run for most FE devs.

### Alternative: Smarter webpack

The best solution would be to make webpack
- recompile the bundles when the CLJS output changes; but
- not try to HMR them (which triggers a browser refresh and ruins CLJS hot reloading)
but I have not found any way to configure this out of the box. I think it would require patching `ReactRefreshPlugin`,
a nontrivial job. It might also be possible for shadow-cljs to poke webpack after it finishes building, but there's the
same problem that webpack will want to HMR.

Webpack doesn't support any "lazy" invalidation; as far as I know there's no
way to say "this bundle is now invalid but don't recompile it until someone
tries to GET it" or anything like that.

### Alternative: Block refresh then continue

The `beforeunload` handler could instead work like this:
- `preventDefault()` on the event, stopping the refresh.
- `fetch()` the `invalidate` route as above.
- asynchronously, when that `fetch()` is done and we know webpack has invalidated the bundles, trigger a refresh.

I haven't tried this, since it seems more complicated than the spin-lock. We just need to give the `invalidate` request
a head start.
parent 86394513
No related branches found
No related tags found
No related merge requests found
......@@ -15,6 +15,18 @@
(devtools/install!)
(js/console.log "CLJS Devtools loaded")
(defn- ^:dev/after-load on-reload
"This currently does nothing, but shadow-cljs warns if there's no `:def/after-load` hook defined."
[])))
(defonce unload-handler-set? (atom false))
(defn- ^:dev/after-load on-reload []
(when (compare-and-set! unload-handler-set? false true)
(js/console.log "CLJS code hot loaded; setting up webpack invalidation on unload")
(.addEventListener js/window "beforeunload"
(fn [_event]
(js/console.log "invalidating webpack build")
(js/fetch "http://localhost:8080/webpack-dev-server/invalidate")
;; HACK: Spin-lock to buy time for webpack to actually start rebuilding. Without this
;; there's a race between the invalidation and the refreshed page loading the bundles.
(let [target (+ (js/performance.now) 500)]
(loop []
(when (< (js/performance.now) target)
(recur))))))))))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment