fix(frames): drain records when a fragment reveals into an adopted region (#2978 follow-up) - #2979
Merged
ryansolid merged 1 commit intoAug 7, 2026
Conversation
…gion A slot invoked inside a server <Loading> ships its `sc:slot:` args record with the deferred fragment — ~the async's own delay after the boundary adopted, long after the adopt-time drain ran. Nothing picked it up: the solidjs#2968 classification gate's only other re-drain arms on `_$HY.fr.pending()`, and the very reveal that DELIVERS the record is what flips that false (post-solidjs#2978 a revealed fragment is no longer pending). The event that makes the record available is the event that disarms the mechanism that would fetch it. So the record stranded in hydration data, and the next full sync — a refetch's stream apply — found a recordless occurrence, classified the region's render prop as a direct-insert VALUE, and handed it to insert as a zero-arg accessor: a props read off `undefined` that halts the reactive system. The reveal callback already exists for the nested-placeholder cascade; it now drains there too (re-drainable by design, so it is a cheap no-op once caught up), and the subscription is no longer gated on `fr.claim`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: db8cdd3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 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 |
ryansolid
added a commit
that referenced
this pull request
Aug 7, 2026
The brenelz round exposed two lifecycle shapes the matrix never pinned: - Byte-identical shell across a switch (dom-expressions#564's policy): slot-driven content ships its differences as records, so the new address's html can be the same bytes — root affinity is per stream, and the identical shell still answers the gate while the record-driven difference lands. - Double-switch mid-flight: the gate re-arms per switch, only the LIVE call's answer settles it, the superseded call's late stream warms its own store without touching the boundary — and switching back re-materializes from that warm synchronously. Plus the reveal-then-refetch cell over an adopted region (#2979's follow-through): the reveal-driven record drain is a one-time repair, and the region stays fully live for the refetch that follows. MATRIX.md gains the address-switch section (the #2977 trio was never tabled) and drops its two GAP rows — both were closed on this branch (shell gate, occurrence-unmount cleanup); the stale test.fails comments go with them. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #2978. A
<Loading>inside a server component now reveals correctly, but the first refetch after that reveal crashes the page:Repro
examples/playground, a server component whose slot is invoked inside its own<Loading>:Load the page, wait for the value, click a button that switches the call's args. The reactive system halts.
Root cause: the delivery disarms its own pickup
A slot invoked inside a server
<Loading>ships itssc:slot:args record with the deferred fragment — about the async's own delay after the boundary adopted, long afteradoptBoundary's drain ran. (Confirmed on the wire: in the document the record's script sits at byte 2708, its$df(...)reveal at 2785 — the record is present and unread.)Nothing ever picks it up:
drainRecords()runs at adopt time — too early, the fragment hasn't streamed._$HY.fr.pending().recordsPending()false.The event that makes the record available is the event that disarms the mechanism that would fetch it. The record strands in
_$HY.rfor the life of the page, and the next full sync — a refetch's stream apply — finds a recordless occurrence, classifies the region's render prop as a direct-insert value, and hands it toinsertas a zero-arg accessor.props.text2onundefinedhalts everything.Fix
The reveal callback already exists in
adoptBoundaryfor the nested-placeholder cascade; it now drains there too. A revealed fragment brings its occurrences' records with it, so the reveal is exactly the right moment.drainRecordsis re-drainable by design (each key applies once), so this is a cheap no-op once caught up. The subscription is also no longer gated onfr.claim— the drain is useful whether or not the ledger exposes claiming.Verification
frames-adopted-region-fragments.spec.tsx, red without the fix and green with it: a record delivered alongside its fragment reaches the frame, so the region's render prop keeps its args.@solidjs/websuite: 400/400. dom-expressions runtime suite: 1114/1114.Note two things this does not fix, both filed/PR'd separately:
text2={test()}) is the DR-2 value tier, and the frame-client mishandles it on repeat responses — fix(frames): wait for a slot record's data refs; async refs compare by identity ryansolid/dom-expressions#565.text#1, a fresh stream shipstext#0), so the first switch re-mounts instead of updating and blanks for the server's wait.$keyon the slot avoids it entirely; the underlying determinism question looks like a design call.🤖 Generated with Claude Code