Repository navigation
fix(web): refuse an async memo that resolves to an element (#4242) - #4255
Conversation
On the server, an async memo (or async iterable source) whose value is
rendered output was handed to the hydration serializer: a boundary's
output (`$lhSkip` accessor) failed in seroval, leaving its record
pending so the hydrating client waited on the memo forever (streamed,
the boundary reverted to its fallback), and an SSR template object
crossed as data (`{t:"<p…>"}`), duplicating the markup and, streamed,
hitting UNRECOGNIZED_INSERT_VALUE on the client.
The memo now rejects where its value lands, before the serializer sees
it, following dynamic()'s DYNAMIC_ASYNC_COMPONENT refusal: new
diagnostic ASYNC_MEMO_ELEMENT (finding on the observe channel with the
memo's owner path; full guidance in dev, the code in prod). The error
takes the normal boundary road: an <Errored> renders its fallback and
the client hydrates it; with none the <Loading> fragment rejects and the
client renders the subtree, exactly as for any other server render
error there.
- Detection (server only, only where the memo serializes): a function
carrying `$lhSkip`, a plain object in the SSRTemplateObject shape (a
lone `t` holding markup, or `t`/`h`/`p` arrays), or a fragment array
holding either. Class instances, Dates, objects with other keys or
with a text `t`, and strings are never refused; <NoHydration> content
is untouched.
- Covers a thenable's landing and an async iterable's first value
(direct, hybrid, and a promise resolving to an iterable).
- settleServerAsync: a landing onSuccess refuses settles as the node's
error instead of an unobserved rejection that left the deferred
pending.
- renderToStream abandonSubtree: skip undefined fragment keys (a
boundary created in an async memo's continuation), which otherwise
turned the refusal into a whole-request failure.
Tests: the issue's nested-boundary shape and a plain JSX element, with
and without <Errored>, pre-shell and streamed, server and hydration
halves (uncontained cases compared against a plain-Error twin), plus
async memos resolving to ordinary data (object, array, string, Date)
that still serialize and are adopted without recompute. The #4246
batch test now expects the outer boundary to reject with the refusal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
🦋 Changeset detectedLatest commit: 4a732da The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Size (brotli, eager entry chunk)
|
Coverage Report for CI Build 40074372338Coverage remained the same at 77.764%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 7.22%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
Summary
Before: on the server, an async memo whose value was rendered output broke hydration. Rendered output here means JSX, a component's result, or a
<Loading>/<Errored>boundary. A boundary's output failed in seroval (SerovalUnsupportedTypeError), so its hydration record never resolved and the client's memo waited forever. When streamed, the boundary showed the server content and then reverted to its fallback for good. A plain template object crossed as data instead: the markup was duplicated, and when streamed the client hitUNRECOGNIZED_INSERT_VALUE. This is what remained of #4242 after #4246.After: the memo rejects where its value lands, with the new diagnostic
ASYNC_MEMO_ELEMENT. Its boundary contains the error like any other render error: the nearest<Errored>renders its fallback, and the client hydrates that. The dev message says what to do instead: return data from the async memo and render the JSX from it.How:
refuseRenderedLandinginpackages/solid/src/server/signals.tsruns where a serialized async value lands: promise values, and the first value of async iterables (all three paths).settleServerAsyncnow turns a throwing landing into the memo's error. Before, it was an unobserved rejection that left the deferred pending.Object.prototypeobjects, arrays of them, and functions marked$lhSkip. Class instances, Dates, Maps, strings and other data are never refused, and neither is content under<NoHydration>orserialize: false.{ t: "<…" }(a single key holding markup) matches a settled template object. Closing that would need a brand on the hotssr()path, so this PR doesn't add one.abandonSubtreeinrenderToStreamskips registry keys that are undefined. A boundary created inside an async memo's continuation registers without an owner id, and that turned one fragment's failure into a whole-request failure.DYNAMIC_ASYNC_COMPONENT: a dev/prod ternary, an observe-channel finding, the RFC-08 table and SKILL.md.diagnostics.jsonis regenerated. The@solidjs/signalschange is type-only.All the new code is server-only; the client bundle is unchanged.
Not covered: later yields of an async iterable (only the first value is checked), and the separate
undefinedids of boundaries created inside a continuation.Research write-up: options measured, including why sending
undefinedor handing off to the client doesn't work when streamed.How did you test this change?
test/{harness,server,hydration}/async-memo-element-4242*cover two shapes: the issue's nested boundary and plain JSX. Each runs with and without<Errored>, settles before the shell or streamed, and is replayed both loaded and streamed.<Errored>catches it when present, there is no seroval error and no markup sent as data, and the request never fails as a whole.<Errored>: the fallback hydrates and there's no halt.<Errored>: each case matches a twin whose memo throws a plainError.{ t: "plain text" }, array, a markup string, a Date) serialize and are adopted without recomputing.nextwithout the fix, all 12 element tests fail on the server side and 12 of 15 hydration tests fail (the data controls pass).stream-batch-serialize-failure-4242.spec.tsx(fix(web): one value that fails to serialize no longer strands the pre-shell hydration stubs (#4242) #4246) now expects the outer_frto reject with the refusal and the inner one to complete.next: the env-onlyserver-functions-adapter-request"rebuild failure", andlone-spread-hydration-key-4240against a stale local compiler binary.dist-nested-manifestsanduncommitted-input-overwrittenfiles.Generated by Claude Code