Repository navigation
Description
Describe the bug
The truncation sweep from #2958 only arms when the client runtime boots while document.readyState === "loading". Otherwise watchTruncation (packages/solid/src/client/hydration.ts) returns early. If hydrate() first runs after parsing is over, there is no DOMContentLoaded left to wait for, and nothing marks the pending _fr declarations. When the stream was cut short, each streamed <Loading> keeps its fallback, hydration never ends, and the boundaries never fetch their data on the client.
The entry can run that late in at least two setups:
- a deferred module entry (
<script type="module" src>withoutasync), which runs atreadyState === "interactive", after parsing and beforeDOMContentLoaded; - an async entry that waits on a route chunk which arrives after
DOMContentLoaded.
I checked the early return on 2.0.0-rc.18, rc.19 and next (81a12d8).
Your Example Website or App
No public link. I hit it in an app on 2.0.0-rc.19 that renders <HydrationScript /> (the early return is the same on rc.18). The harness repro below shows the same thing.
Steps to Reproduce the Bug or Issue
In the app: hold the route chunk until after DOMContentLoaded and cut the document stream after the shell. When the entry runs, the _fr refs still have no .s, hydration never ends and no data is fetched.
In this repo, with the late-boundary-after-done artifact from the parity harness (shell only, the rest never arrives):
globalThis._$HY = { events: [], completed: new WeakSet(), r: {}, s: { "": 1 }, fe() {} };
Object.defineProperty(document, "readyState", { value: "interactive", configurable: true });
container.innerHTML = shell.replace(scriptRe, "");
for (const m of shell.matchAll(scriptRe)) (0, eval)(m[1]);
hydrate(() => <scenario.App />, container);
await sleep(400);
_$HY.r["40_fr"].s; // undefined
_$HY.done; // undefinedThe same shell with readyState at "loading" during hydrate() and a DOMContentLoaded afterwards (truncated-stream.spec.tsx) ends with the fragment rejected and _$HY.done === true.
Expected behavior
Same result as a runtime that booted mid-stream: the unsettled declaration becomes a rejected fragment, the boundary releases through the rejection path, the pending registry refs are swept, and hydration finishes.
Platform
- solid-js / @solidjs/web: 2.0.0-rc.18 (also rc.19 and
next) - Harness repro: jsdom
Additional context
The comment above watchTruncation says a runtime loaded after parse can't tell a completed page from a truncated one. With the in-render bootstrap (_$HY.s, #3971), every _fr declaration and every settle is a parser-inserted script, so once parsing is over a pending _fr can't settle anymore. A complete page has nothing pending, so a sweep there finds nothing to mark.
Once hydration ends, the boundary still keeps its fallback, because its memo adopts the dead promise. That part is #4413, which applies to the DOMContentLoaded path too.
I have a fix that runs the same sweep at boot in that case and keeps the order the comments describe (fragment pass first, registry pass a macrotask later). PR coming.
Activity
ryansolid commentedon Oct 30, 2026
Fixed on
nextby #4416 (99d9821); it ships in the next RC.Generated by Claude Code