Repository navigation
Description
Summary
On the server, a Loading boundary whose async memo resolves to another Loading boundary (an Element) streams a document in which none of the hydration batch's boundaries ever resolves on rc.18. On rc.17 only the outer memo's own value was left unresolved, and the boundaries themselves completed.
The outer memo's value is the inner boundary's function ($lhSkip), which seroval cannot serialize, and renderToStream logs SerovalUnsupportedTypeError on both versions. On rc.17 that error drops just that key. On rc.18 the same error drops every value written in the same $B batch, including both boundaries' completion flags (_fr) and the inner memo's value, which serialized fine. The client then waits forever on those boundaries.
seroval 1.7.1 forced through overrides behaves like rc.18's pinned 1.7.0, so the change looks like how rc.18's writer batches values, with seroval 1.7 ending a write at its first failing value.
Reproduction
In a fresh directory, install one version pair:
npm install --save-exact solid-js@2.0.0-rc.17 @solidjs/web@2.0.0-rc.17
# Then repeat in a separate directory with both packages at 2.0.0-rc.18.
node --conditions=node repro.mjsrepro.mjs:
import { createComponent, createMemo, Loading } from "solid-js";
import { renderToStream } from "@solidjs/web";
// A Loading boundary whose async memo resolves to a value.
function slot(read, fallback) {
return createComponent(Loading, {
fallback,
get children() {
const value = createMemo(async () => read());
return [() => value()];
}
});
}
const flat = () => slot(async () => "flat-41", "outer");
// The outer memo resolves to an inner Loading boundary (an Element).
const nested = () =>
slot(() => Promise.resolve(1).then(v => slot(async () => `nested-${v * 41}`, "inner")), "outer");
function render(app) {
return new Promise(resolve => {
let text = "";
renderToStream(app).pipe({ write: c => void (text += c), end: () => resolve(text) });
});
}
for (const [name, app] of [["flat", flat], ["nested", nested]]) {
const text = await render(app);
const keys = [...text.matchAll(/"?([\w]+)"?:\$R\[(\d+)\]=\(\$R\[(\d+)\]=/g)].map(m => [m[1], m[3]]);
const settled = new Set([...text.matchAll(/\(\$R\[(\d+)\],/g)].map(m => m[1]));
console.log(`${name}: body ${JSON.stringify(text.slice(0, text.indexOf("<script>")))}`);
for (const [key, ref] of keys) console.log(` ${key}: ${settled.has(ref) ? "resolved" : "NEVER RESOLVED"}`);
}Output
rc.17:
flat: body "flat-41"
0_fr: resolved
0000: resolved
SerovalUnsupportedTypeError: Seroval Error (specific: 1)
value: [Function (anonymous)] { '$lhSkip': true }
nested: body "nested-41"
0_fr: resolved
0000: NEVER RESOLVED
undefined_fr: resolved
undefined000: resolved
rc.18 (and rc.18 with seroval 1.7.1):
flat: body "flat-41"
0_fr: resolved
0000: resolved
SerovalUnsupportedTypeError: Seroval Error (specific: 1)
value: [Function (anonymous)] { '$lhSkip': true }
nested: body "nested-41"
0_fr: NEVER RESOLVED
0000: NEVER RESOLVED
undefined_fr: NEVER RESOLVED
undefined000: NEVER RESOLVED
The raw rc.18 tail is ..._$HY.r["$B"]=($R[0]={"0_fr":...,"0000":...,undefined_fr:...,undefined000:...});(b=>{...})(_$HY.r["$B"]);</script> with no resolver calls after it, where rc.17 ends with ($R[9],"nested-41");$R[11]($R[2],!0);$R[11]($R[7],!0);.
The same rule reaches values an app writes itself. Two writes in one tick, one resolving to a function, drop the other on rc.18 and stream it on rc.17:
import { renderToStream, getHydrationWriter } from "@solidjs/web";
const app = () => {
const writer = getHydrationWriter();
writer.write("bad", Promise.resolve(() => 10));
writer.write("fine", Promise.resolve("still here"));
return "odd";
};
const text = await new Promise(resolve => {
let t = "";
renderToStream(app).pipe({ write: c => void (t += c), end: () => resolve(t) });
});
console.log(`"still here" streamed: ${text.includes("still here")}`);This prints true on rc.17 and false on rc.18, after the same SerovalUnsupportedTypeError on both.
Expected
A value that cannot be serialized fails only its own key, as on rc.17, and the boundaries in the same batch still complete. Separately, the inner boundary's keys read undefined_fr and undefined000 on both versions, which looks like a missing id for a boundary created inside a resolved memo.
Environment
Node 26.10.0, solid-js and @solidjs/web 2.0.0-rc.17 and 2.0.0-rc.18 (seroval 1.6.7 and 1.7.0 as each pins), Linux.
Activity
lovrozagar commentedon Oct 23, 2026
I'm working on a fix for the batch isolation (a value that fails to serialize drops only its own key, as on rc.17) and will open a PR against
nextshortly. Theundefinedboundary ids are a separate, older problem; I'm leaving them out of that PR.fix(bit-00): solid-js and @solidjs/web peers stop below 2.0.0-rc.18 (…
fix(bit-04): a live document's deferred value the wire cannot hold is…
fix(web): one value that fails to serialize no longer strands the pre…
ryansolid commentedon Oct 24, 2026
Fixed by #4246 (merged to
next): pre-shell stubs are now written per key, so one value seroval cannot serialize only leaves its own key pending, as on rc.17. The outer memo value in the nested repro (a function) still does not resolve; that is the rc.17 behaviour and a separate problem from this regression.Generated by Claude Code
fix(web): refuse an async memo that resolves to an element (#4242) (#…
test(web): #4316's parked late case and twin warnings under adopted f…
fix(solid,web): a rejected fragment's fresh render creates Dynamic ta…
fix(solid): a rejected fragment's fresh render claims what the server…
fix(web): split #4647 at the first flush: fail the render before it, …
fix(web): reject a hydration key whose async value fails to serialize (…