You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Versions:solid-js / @solidjs/web 2.0.0-beta.31 at dc7b5c2, @dom-expressions/runtime 0.50.0-next.37, vite-plugin-solid 3.0.0-next.22, examples/playground turnkey setup (ssr: {}, serverFunctions: { components: true }). Related: #2977 (found while debugging the same playground), #2964 (whose fix introduced the held-swap policy this interacts with).
Repro
exportconstgetServerComp=async(count: number)=>{"use server";return()=>{consttest=createMemo(async()=>{awaitnewPromise(r=>setTimeout(r,1000));returnMath.random();});return(<div><h1>Playground2</h1><Loadingfallback={<div>loading new value…</div>}>{test()}</Loading></div>);};};exportdefaultfunctionApp(){const[count,setCount]=createSignal(0);constServerComp=dynamic(()=>getServerComp(count()));return(<Loadingfallback={<div>Loading...</div>}><ServerComp/><buttononClick={()=>setCount(count()+1)}>Increment</button></Loading>);}
Load the page. The shell streams in with loading new value…, and it never reveals — the fallback stays on screen forever. The client-slot machinery is not involved (plain {test()} reproduces it).
What the wire and DOM show
The server does everything right. The document stream completes at ~1s and contains the placeholder (<template id="pl-11"> + fallback), the content template (<template id="11"> with the resolved markup), and the reveal call $df("11").
Client-side after load (all verified in-page):
placeholder and content template both still in the DOM
_$HY.done === true, _$HY.fr.pending() === true
_$HY.f("11") returns 0 — the swap was held
manually calling $dfr("11") swaps the content in correctly
The frames document adoption (adoptBoundary, solid-web/frames/src/client.ts) anticipates held fragments: its recordsPending() seam answers "may the document still deliver records?" with _$HY.fr.pending(), and a recordless occurrence defers, re-checking until that flips false (the comment at the seam even notes "a held fragment's replay can still deliver one").
Nothing on the adoption side ever claims — _$HY.fr only exposes { pending, subscribe }; there is no claim for an integration to call. So the ledger holds the swap waiting for a claimant, the template/placeholder stay in the DOM, fragmentPending() stays true, the adoption's occurrence defers forever. Deadlock; the fallback wedges.
The timing window explains why ordinary streamed boundaries are fine: an inline $df executing during parse (before the module that installs _$HY.f loads) swaps via $dfr directly. The wedge needs the fragment to settle after client boot — which any real data fetch does.
The policy is right to hold — the claimant is what's missing
Force-revealing the held fragment with an unclaimed $dfr("11") swaps the markup in but orphans the region permanently: subsequent refetches POST, the server renders and responds, and the response is never applied. That is exactly the #2964 inert-nodes scenario the policy exists to prevent — so the fix is not to weaken the hold.
Meanwhile the frame-stream path is fully correct: clicking Increment on a wedged page recovers it — the refetch response (start → html → slot → fragment → reveal → complete) applies cleanly and removes the stale placeholder. Only the document-SSR reveal path has no claimant.
Secondary defect
_$HY.fr.pending() stays true for the rest of the page's life even after a refetch replaces the wedged region — the document fragment's ledger entry never resolves. Anything consulting boundaryMayArrive() / recordsPending() keeps reading "the document may still deliver" indefinitely.
Suggested direction
Expose claim(id) on _$HY.fr alongside pending/subscribe, and have adoptBoundary (or the frame's occurrence classification, which already knows claimScope: id and the adopted subtree) claim fragments whose pl-* placeholders sit inside the markup it adopts. The claim replays the held swap before the occurrence re-checks, and the existing installRevealHook subscription already re-indexes on reveal.
Dev-tooling footnote (separate, vite-plugin-solid): during debugging, vite dev repeatedly served a stale SSR copy of the "use server" module across edits until a full server restart — the stream contained markup already deleted from the source. Worth its own look if others hit "my server component won't update".
Versions:
solid-js/@solidjs/web2.0.0-beta.31 at dc7b5c2,@dom-expressions/runtime0.50.0-next.37,vite-plugin-solid3.0.0-next.22,examples/playgroundturnkey setup (ssr: {},serverFunctions: { components: true }). Related: #2977 (found while debugging the same playground), #2964 (whose fix introduced the held-swap policy this interacts with).Repro
Load the page. The shell streams in with
loading new value…, and it never reveals — the fallback stays on screen forever. The client-slot machinery is not involved (plain{test()}reproduces it).What the wire and DOM show
The server does everything right. The document stream completes at ~1s and contains the placeholder (
<template id="pl-11">+ fallback), the content template (<template id="11">with the resolved markup), and the reveal call$df("11").Client-side after load (all verified in-page):
_$HY.done === true,_$HY.fr.pending() === true_$HY.f("11")returns0— the swap was held$dfr("11")swaps the content in correctlyRoot cause: two seams each waiting for the other
fragmentPolicy, introduced by the Server component that settles after the shell flush never mounts (blank region, no fallback, no error) #2964 fix): a$dfarriving after global hydration completes is held until a claimant registers viaclaimFragment— which is only ever called by a client-hydrated<Loading>boundary (hydration.ts:1844).adoptBoundary, solid-web/frames/src/client.ts) anticipates held fragments: itsrecordsPending()seam answers "may the document still deliver records?" with_$HY.fr.pending(), and a recordless occurrence defers, re-checking until that flips false (the comment at the seam even notes "a held fragment's replay can still deliver one")._$HY.fronly exposes{ pending, subscribe }; there is noclaimfor an integration to call. So the ledger holds the swap waiting for a claimant, the template/placeholder stay in the DOM,fragmentPending()stays true, the adoption's occurrence defers forever. Deadlock; the fallback wedges.The timing window explains why ordinary streamed boundaries are fine: an inline
$dfexecuting during parse (before the module that installs_$HY.floads) swaps via$dfrdirectly. The wedge needs the fragment to settle after client boot — which any real data fetch does.The policy is right to hold — the claimant is what's missing
Force-revealing the held fragment with an unclaimed
$dfr("11")swaps the markup in but orphans the region permanently: subsequent refetches POST, the server renders and responds, and the response is never applied. That is exactly the #2964 inert-nodes scenario the policy exists to prevent — so the fix is not to weaken the hold.Meanwhile the frame-stream path is fully correct: clicking Increment on a wedged page recovers it — the refetch response (
start → html → slot → fragment → reveal → complete) applies cleanly and removes the stale placeholder. Only the document-SSR reveal path has no claimant.Secondary defect
_$HY.fr.pending()staystruefor the rest of the page's life even after a refetch replaces the wedged region — the document fragment's ledger entry never resolves. Anything consultingboundaryMayArrive()/recordsPending()keeps reading "the document may still deliver" indefinitely.Suggested direction
Expose
claim(id)on_$HY.fralongsidepending/subscribe, and haveadoptBoundary(or the frame's occurrence classification, which already knowsclaimScope: idand the adopted subtree) claim fragments whosepl-*placeholders sit inside the markup it adopts. The claim replays the held swap before the occurrence re-checks, and the existinginstallRevealHooksubscription already re-indexes on reveal.Dev-tooling footnote (separate, vite-plugin-solid): during debugging, vite dev repeatedly served a stale SSR copy of the
"use server"module across edits until a full server restart — the stream contained markup already deleted from the source. Worth its own look if others hit "my server component won't update".