Commit 32bd92a
committed
fix: Drop events raised before the vtree is mounted (hydration race)
## Problem
On server-rendered pages (e.g. haskell-miso.org) the browser console
fills with repeated errors while the wasm app is hydrating:
Uncaught TypeError: Cannot read properties of null (reading 'type')
at delegateEvent (ghc_wasm_jsffi.js:1038:13)
at dispatch (ghc_wasm_jsffi.js:1016:5)
`delegateEvent` reads `obj.type`, where `obj` is the vtree -- and the
vtree is `null`.
## The race
`Miso.Runtime.initialize` does, in program order:
1. `_componentVTree <- newIORef (VTree (Object jsNull))` -- the vtree
ref starts out null.
2. `delegator _componentDOMRef _componentVTree events ...` -- DOM
listeners for every delegated event attach to the mount point,
synchronously, via the FFI.
3. `initialDraw` -- only now is the vtree built and `Hydrate.hydrate`
run, and only after hydration succeeds is the ref written.
Steps 2 and 3 are not one atomic JS task. The wasm backend runs Haskell
as promise continuations on the browser event loop, yielding at every
JSFFI round-trip and scheduler timeslice. `initialDraw` builds the whole
vtree and walks the whole server-rendered DOM, so the window between
"listeners live" and "vtree written" spans many event-loop turns.
Meanwhile SSR means the page is painted and interactive-looking before
the wasm even finishes loading -- the user is already mousing and
clicking. Every event delivered inside that window runs:
listener -> getVTree -> readIORef (still jsNull)
-> delegateEvent(event, null, stack, ...) -> null.type
Mouse events fire in bursts as the cursor crosses elements, hence the
flood of identical errors. The invariant the delegator assumed -- "if my
listeners are installed, the vtree exists" -- only holds in the non-SSR
path, where the first draw paints the DOM before the user can interact.
Hydration breaks it because the DOM (and the user's input stream) exists
before miso does.
## Fix
Short-circuit upstream in `listener` (ts/miso/event.ts): if the vtree
has not been stored yet, drop the event, with a warning in debug mode.
Dropping is semantically correct -- pre-hydration there are no handlers
in the vtree, so those events had no observable behavior to lose.
The Lynx BTS delegator (ts/miso/native/bts/context.ts) bypasses
`listener` and calls `delegateEvent` directly against the same
jsNull-initialized ref, so it gets the identical guard.
Installing the delegator before hydration is kept intentionally:
installing it after would drop the same events anyway (silently, at the
browser level, since no listener exists yet), and early installation
leaves the door open to queueing and replaying pre-hydration events
later.
## Testing
- New regression test in ts/spec/event.spec.ts installs the delegator
with `getVTree` yielding null and clicks server-rendered markup.
Listener exceptions do not propagate to the dispatching `.click()` --
they surface as `error` events on `window` -- so the test captures
those and asserts none fire. Verified the test fails with exactly the
reported TypeError when the guard is removed.
- `bun test ts/spec/event.spec.ts ts/spec/native-bts.spec.ts`: 43 pass,
0 fail.
- `bun run js`: all four shipped bundles (js/miso.js, js/miso.prod.js,
js/miso-native.js, js/miso-native.prod.js) regenerated and each now
contains the guard.1 parent 877496a commit 32bd92a
7 files changed
Lines changed: 52 additions & 3 deletions
File tree
- js
- ts
- miso
- native/bts
- spec
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
865 | 865 | | |
866 | 866 | | |
867 | 867 | | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
868 | 874 | | |
869 | 875 | | |
870 | 876 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
31 | 40 | | |
32 | 41 | | |
33 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
48 | 57 | | |
49 | 58 | | |
50 | 59 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
76 | 101 | | |
77 | 102 | | |
78 | 103 | | |
| |||
0 commit comments