Repository navigation
fix(solid,web): a failed Errored waits for its fallback's async holes before streaming - #4308
Conversation
…e, and adopt it (#4287) Outside a server component, a <Loading> whose async read failed during a streamed render settled its fragment blank, and the hydrating client rendered the failed boundary from scratch: it asked again and showed the loading fallback. Server: the failing <Loading> now asks the enclosing <Errored> for its outcome outside server components too, passing its own id. The Errored answers only when that Loading is all of its content (its resolved markup is the Loading's placeholder): its fallback is drawn into the fragment, or inlined pre-flush, and the error is serialized at the Errored id. Deeper in the content it answers nothing, as before. A rejected fragment's template is marked `e`, and `$dfr` records the nodes it swaps in for it. Client: inside a hydrated Errored's no-claim content run, a <Loading> runs its content with its data adopted instead of waiting on its fragment, so the Errored holds the client's own error and tracks the content's inputs (#4273). A boundary that claimed its fallback and then renders a rejected fragment fresh clears the nodes the swap put in first, so nothing of the server's outcome is left beside the client's render. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…p is sent When a streamed <Loading> fails after the shell and asks the enclosing <Errored> for its outcome, the fallback may hold an async hole that has not resolved yet. renderOutcome then returns no markup (the fragment settles as a plain rejection), but handleError had already serialized the error at the Errored id. A client hydrating after the failure read that record, claimed a fallback that was never sent, and went blank with a hydration key miss. The outcome path now defers the record and writes it only once the fallback resolved to finished markup. Without it the client renders the failure itself, as on `next`. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
…resolves (#4306) An <Errored> whose fallback reads async data, around a <Loading> that fails after the shell inside an outer <Loading> still pending, lost its region: the outcome render met the fallback's pending hole, answered no markup, and the parked fragment spliced a blank into the outer one while its `_fr` resolved. The client claimed against the blank and showed nothing there, with hydration warnings. renderOutcome now answers a still-resolving fallback with a function that waits out its holes and returns the finished markup, serializing the error only then. The <Loading> calls it only while its fragment is parked in an enclosing pending one (the registry's new `parked()` query on the settle function), where nothing of the failure shows yet: the outer fragment carries the fallback, or, if the outer settles first, the inner fragment streams it when it lands. A failure in a region already shown settles at once, as before. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 584e145 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 40410527484Coverage remained the same at 78.589%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
…c holes (#4306) Per RULES-DERIVED D-18 (a swap to a fallback waits for the fallback to be ready), the wait for an Errored fallback's async holes no longer depends on the fragment being parked. A <Loading> that fails after the shell under an <Errored> whose fallback is still resolving keeps its placeholder until the finished fallback is ready, then streams it with the error record, in the parked case (carried by the outer fragment while that is pending) and the unparked one alike. The registry's `parked()` query is dropped. #4303's async-fallback scenario now expects the server's finished fallback and its record: a client hydrating after the failure arrived claims it and asks nothing again. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…-x1ore2-4306 # Conflicts: # packages/solid/src/server/hydration.ts # packages/solid/src/server/signals.ts # packages/web/test/harness/__artifacts__/errored-loading-postflush-rejection-async-fallback-4287.json # packages/web/test/harness/scenarios.tsx # packages/web/test/hydration/errored-async-outcome-4287.spec.tsx
Merging this PR will improve performance by 15.64%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
A served answer's arrival is hydration's own pass (NO_CLAIM): a latched node re-running there re-serves its server answer. A client write carried by that same flush must still make the node compute. - Client writes already made (pending, unflushed) flush first, outside the pass (`servedPass`). The async runtime's landing flushes synchronously, and that flush would otherwise carry them. - The pass covers only the flush round its served write lands in (`_servedAt = clock`). An effect's write runs in a later round of the same flush, and a latched node re-run there computes. The latch tests `clock > _servedAt`. `@solidjs/signals` exports its flush-round counter (`clock`), an internal live binding, at 0 B. Landings cannot be coalesced into one pass per microtask batch without changing signals. `asyncWrite` already flushes synchronously on every landing (`schedule(); flush()`) at the #4308 head, so a served pass adds no flush per landing. Its trailing `flush()` is a no-op after a value landing. Tests: in served-source-landing, a write before the flush and an effect's write in the landing's flush both give the written value with 1 ask. Both fail on 2b2990a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…dy sent (#4313) * fix(solid,web): draw an Errored fallback for a Loading's async failure, and adopt it (#4287) Outside a server component, a <Loading> whose async read failed during a streamed render settled its fragment blank, and the hydrating client rendered the failed boundary from scratch: it asked again and showed the loading fallback. Server: the failing <Loading> now asks the enclosing <Errored> for its outcome outside server components too, passing its own id. The Errored answers only when that Loading is all of its content (its resolved markup is the Loading's placeholder): its fallback is drawn into the fragment, or inlined pre-flush, and the error is serialized at the Errored id. Deeper in the content it answers nothing, as before. A rejected fragment's template is marked `e`, and `$dfr` records the nodes it swaps in for it. Client: inside a hydrated Errored's no-claim content run, a <Loading> runs its content with its data adopted instead of waiting on its fragment, so the Errored holds the client's own error and tracks the content's inputs (#4273). A boundary that claimed its fallback and then renders a rejected fragment fresh clears the nodes the swap put in first, so nothing of the server's outcome is left beside the client's render. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid): serialize an Errored's error only when its fallback markup is sent When a streamed <Loading> fails after the shell and asks the enclosing <Errored> for its outcome, the fallback may hold an async hole that has not resolved yet. renderOutcome then returns no markup (the fragment settles as a plain rejection), but handleError had already serialized the error at the Errored id. A client hydrating after the failure read that record, claimed a fallback that was never sent, and went blank with a hydration key miss. The outcome path now defers the record and writes it only once the fallback resolved to finished markup. Without it the client renders the failure itself, as on `next`. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 * fix(solid,web): stream a parked Errored fallback once its async hole resolves (#4306) An <Errored> whose fallback reads async data, around a <Loading> that fails after the shell inside an outer <Loading> still pending, lost its region: the outcome render met the fallback's pending hole, answered no markup, and the parked fragment spliced a blank into the outer one while its `_fr` resolved. The client claimed against the blank and showed nothing there, with hydration warnings. renderOutcome now answers a still-resolving fallback with a function that waits out its holes and returns the finished markup, serializing the error only then. The <Loading> calls it only while its fragment is parked in an enclosing pending one (the registry's new `parked()` query on the settle function), where nothing of the failure shows yet: the outer fragment carries the fallback, or, if the outer settles first, the inner fragment streams it when it lands. A failure in a region already shown settles at once, as before. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid,web): a failed Errored always waits for its fallback's async holes (#4306) Per RULES-DERIVED D-18 (a swap to a fallback waits for the fallback to be ready), the wait for an Errored fallback's async holes no longer depends on the fragment being parked. A <Loading> that fails after the shell under an <Errored> whose fallback is still resolving keeps its placeholder until the finished fallback is ready, then streams it with the error record, in the parked case (carried by the outer fragment while that is pending) and the unparked one alike. The registry's `parked()` query is dropped. #4303's async-fallback scenario now expects the server's finished fallback and its record: a client hydrating after the failure arrived claims it and asks nothing again. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid,web): a hydrated failure re-serves the server's error instead of asking again (#4287, #4306) A client hydrating mid-stream asked again for an async memo whose server read failed, and flashed back to the inner <Loading> fallback after the failure showed: Errored > Loading > read, with the read failing after the shell, unparked (the fragment rejects and the client renders the failure fresh over the swapped-in Errored fallback) or parked in a pending outer Loading (the outer resume runs the hydrated Errored's content, NO_CLAIM). Both content runs read the shell memo a cycle after its error landed, and a later tracked read retries an errored async node (PJ-R35). The retry re-enters the hydration latch, which since f3da41e has taken every re-entry for a dependency change ("re-entry is always invalidation- driven"), and since b68a907 computes such a rerun at once for a shell node outside the claim in progress: one real ask, the Loading fallback for its duration. Before f3da41e the latch re-served the serialized answer on every re-entry while hydrating, so the retry asked nothing. Inside a hydrated failure's content run (NO_CLAIM) a latched rerun now re-serves the server's answer, its error included, and arms nothing. A rejected fragment's fresh render runs as such a content run when the swap brought the outcome the server drew for the failure (the reveal record lists its nodes); to make that list the outcome alone, a rejected fragment with no outcome streams an empty template instead of a lone space. Uncontained failures (no outcome) keep rendering fresh as before. Tests: the #4287/#4306 hydration specs stream piece by piece with a clock tick between pieces (the plain-fallback case only passed because no flush ran between the error landing and the fresh read), assert 0 asks in both modes and no flash back to a loading fallback, and cover the parked plain-fallback scenario, which now has an ask counter and an update pass (the Errored tracks the memo, #4273). Size: 0 B on every CSR scenario (compiled floor 9,966 B); +65-71 B minified on the hydrating scenarios. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid): a served source's arrival re-serves its dependents' answers instead of asking again Hydrated mid-stream, an async shell memo `b` reading another, `a`, both read only inside a streamed <Loading>, asked again when `a`'s server answer arrived: the landing re-ran `b`, and the hydration latch took the rerun for a client write (f3da41e's "re-entry is always invalidation- driven"; b68a907 computes it at once for a shell node). The server's `b` was computed from that same `a`; nothing on the client diverged. The latch now has one rule: a latched rerun inside hydration's own pass over served answers (NO_CLAIM) re-serves its own answer and arms nothing. That pass is a hydrated failure's content run (the Errored's, or a rejected fragment's fresh render over the outcome the server drew, from the previous commit) and, new here, a served answer's arrival: readHydratedValue hands the runtime a thenable whose landing runs, with the flush that carries it, under NO_CLAIM. The NO_CLAIM check moves to the head of the latch rule, so in-claim and held reruns there re-serve without arming a takeover too. Client writes still compute (the write-before-resume, nav-before-resume and hydration-latch pins), and a retry outside that pass still follows the client rule. Tests: a parity scenario and spec for the b-reads-a shape (0 asks loaded and streamed piece by piece with clock ticks, one real ask after a client write to their input); both fail before this commit. Size: 0 B on every CSR scenario (compiled floor 9,966 B); +19-27 B minified on the hydrating scenarios over the previous commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid): a served pass never swallows a client write A served answer's arrival is hydration's own pass (NO_CLAIM): a latched node re-running there re-serves its server answer. A client write carried by that same flush must still make the node compute. - Client writes already made (pending, unflushed) flush first, outside the pass (`servedPass`). The async runtime's landing flushes synchronously, and that flush would otherwise carry them. - The pass covers only the flush round its served write lands in (`_servedAt = clock`). An effect's write runs in a later round of the same flush, and a latched node re-run there computes. The latch tests `clock > _servedAt`. `@solidjs/signals` exports its flush-round counter (`clock`), an internal live binding, at 0 B. Landings cannot be coalesced into one pass per microtask batch without changing signals. `asyncWrite` already flushes synchronously on every landing (`schedule(); flush()`) at the #4308 head, so a served pass adds no flush per landing. Its trailing `flush()` is a no-op after a value landing. Tests: in served-source-landing, a write before the flush and an effect's write in the landing's flush both give the written value with 1 ask. Both fail on 2b2990a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * perf(solid): golf the served-pass client code This changes no behaviour. Client bytes over next go from about +130 B to +58 B minified. - A hydrated `<Errored>`'s content run and a served pass are one window kind: `_claimOwner = ~clock`, a window over no owner for the flush round it opened in. `~` keeps it truthy when clock is 0. - That replaces both `NO_CLAIM` and `_servedAt`. The latch checks one thing, `_claimOwner !== ~clock`, and a later round of an arrival's flush (an effect's write) still computes. `isClaiming` reads a number as no owner, so nothing claims, as before. - `servedPass` is curried (`servedPass(f)(x)`), so the arrival thenable is `initP.then(servedPass(a), servedPass(b))`. - A resume clears the hydration flag once, before either branch. The fresh branch's own flush was redundant with the resume's trailing one. The outcome check reads `?.[0]` instead of `?.length`. The pre-flush stays on both paths. Removing it makes the write-before-landing test fail, and comparing only `_claimOwner` without the round makes the effect-write test fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * perf(solid): read the page's _$HY directly on the recover path A recover only runs while hydrating, so the guarded globalThis read is dead weight: -12 B minified on every hydrating and page scenario. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 --------- Co-authored-by: Claude <noreply@anthropic.com>
… sent (#4319) * fix(solid,web): draw an Errored fallback for a Loading's async failure, and adopt it (#4287) Outside a server component, a <Loading> whose async read failed during a streamed render settled its fragment blank, and the hydrating client rendered the failed boundary from scratch: it asked again and showed the loading fallback. Server: the failing <Loading> now asks the enclosing <Errored> for its outcome outside server components too, passing its own id. The Errored answers only when that Loading is all of its content (its resolved markup is the Loading's placeholder): its fallback is drawn into the fragment, or inlined pre-flush, and the error is serialized at the Errored id. Deeper in the content it answers nothing, as before. A rejected fragment's template is marked `e`, and `$dfr` records the nodes it swaps in for it. Client: inside a hydrated Errored's no-claim content run, a <Loading> runs its content with its data adopted instead of waiting on its fragment, so the Errored holds the client's own error and tracks the content's inputs (#4273). A boundary that claimed its fallback and then renders a rejected fragment fresh clears the nodes the swap put in first, so nothing of the server's outcome is left beside the client's render. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid): serialize an Errored's error only when its fallback markup is sent When a streamed <Loading> fails after the shell and asks the enclosing <Errored> for its outcome, the fallback may hold an async hole that has not resolved yet. renderOutcome then returns no markup (the fragment settles as a plain rejection), but handleError had already serialized the error at the Errored id. A client hydrating after the failure read that record, claimed a fallback that was never sent, and went blank with a hydration key miss. The outcome path now defers the record and writes it only once the fallback resolved to finished markup. Without it the client renders the failure itself, as on `next`. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 * fix(solid,web): stream a parked Errored fallback once its async hole resolves (#4306) An <Errored> whose fallback reads async data, around a <Loading> that fails after the shell inside an outer <Loading> still pending, lost its region: the outcome render met the fallback's pending hole, answered no markup, and the parked fragment spliced a blank into the outer one while its `_fr` resolved. The client claimed against the blank and showed nothing there, with hydration warnings. renderOutcome now answers a still-resolving fallback with a function that waits out its holes and returns the finished markup, serializing the error only then. The <Loading> calls it only while its fragment is parked in an enclosing pending one (the registry's new `parked()` query on the settle function), where nothing of the failure shows yet: the outer fragment carries the fallback, or, if the outer settles first, the inner fragment streams it when it lands. A failure in a region already shown settles at once, as before. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid,web): a failed Errored always waits for its fallback's async holes (#4306) Per RULES-DERIVED D-18 (a swap to a fallback waits for the fallback to be ready), the wait for an Errored fallback's async holes no longer depends on the fragment being parked. A <Loading> that fails after the shell under an <Errored> whose fallback is still resolving keeps its placeholder until the finished fallback is ready, then streams it with the error record, in the parked case (carried by the outer fragment while that is pending) and the unparked one alike. The registry's `parked()` query is dropped. #4303's async-fallback scenario now expects the server's finished fallback and its record: a client hydrating after the failure arrived claims it and asks nothing again. Server-only; no client bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid,web): a hydrated failure re-serves the server's error instead of asking again (#4287, #4306) A client hydrating mid-stream asked again for an async memo whose server read failed, and flashed back to the inner <Loading> fallback after the failure showed: Errored > Loading > read, with the read failing after the shell, unparked (the fragment rejects and the client renders the failure fresh over the swapped-in Errored fallback) or parked in a pending outer Loading (the outer resume runs the hydrated Errored's content, NO_CLAIM). Both content runs read the shell memo a cycle after its error landed, and a later tracked read retries an errored async node (PJ-R35). The retry re-enters the hydration latch, which since f3da41e has taken every re-entry for a dependency change ("re-entry is always invalidation- driven"), and since b68a907 computes such a rerun at once for a shell node outside the claim in progress: one real ask, the Loading fallback for its duration. Before f3da41e the latch re-served the serialized answer on every re-entry while hydrating, so the retry asked nothing. Inside a hydrated failure's content run (NO_CLAIM) a latched rerun now re-serves the server's answer, its error included, and arms nothing. A rejected fragment's fresh render runs as such a content run when the swap brought the outcome the server drew for the failure (the reveal record lists its nodes); to make that list the outcome alone, a rejected fragment with no outcome streams an empty template instead of a lone space. Uncontained failures (no outcome) keep rendering fresh as before. Tests: the #4287/#4306 hydration specs stream piece by piece with a clock tick between pieces (the plain-fallback case only passed because no flush ran between the error landing and the fresh read), assert 0 asks in both modes and no flash back to a loading fallback, and cover the parked plain-fallback scenario, which now has an ask counter and an update pass (the Errored tracks the memo, #4273). Size: 0 B on every CSR scenario (compiled floor 9,966 B); +65-71 B minified on the hydrating scenarios. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid): a served source's arrival re-serves its dependents' answers instead of asking again Hydrated mid-stream, an async shell memo `b` reading another, `a`, both read only inside a streamed <Loading>, asked again when `a`'s server answer arrived: the landing re-ran `b`, and the hydration latch took the rerun for a client write (f3da41e's "re-entry is always invalidation- driven"; b68a907 computes it at once for a shell node). The server's `b` was computed from that same `a`; nothing on the client diverged. The latch now has one rule: a latched rerun inside hydration's own pass over served answers (NO_CLAIM) re-serves its own answer and arms nothing. That pass is a hydrated failure's content run (the Errored's, or a rejected fragment's fresh render over the outcome the server drew, from the previous commit) and, new here, a served answer's arrival: readHydratedValue hands the runtime a thenable whose landing runs, with the flush that carries it, under NO_CLAIM. The NO_CLAIM check moves to the head of the latch rule, so in-claim and held reruns there re-serve without arming a takeover too. Client writes still compute (the write-before-resume, nav-before-resume and hydration-latch pins), and a retry outside that pass still follows the client rule. Tests: a parity scenario and spec for the b-reads-a shape (0 asks loaded and streamed piece by piece with clock ticks, one real ask after a client write to their input); both fail before this commit. Size: 0 B on every CSR scenario (compiled floor 9,966 B); +19-27 B minified on the hydrating scenarios over the previous commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid): a served pass never swallows a client write A served answer's arrival is hydration's own pass (NO_CLAIM): a latched node re-running there re-serves its server answer. A client write carried by that same flush must still make the node compute. - Client writes already made (pending, unflushed) flush first, outside the pass (`servedPass`). The async runtime's landing flushes synchronously, and that flush would otherwise carry them. - The pass covers only the flush round its served write lands in (`_servedAt = clock`). An effect's write runs in a later round of the same flush, and a latched node re-run there computes. The latch tests `clock > _servedAt`. `@solidjs/signals` exports its flush-round counter (`clock`), an internal live binding, at 0 B. Landings cannot be coalesced into one pass per microtask batch without changing signals. `asyncWrite` already flushes synchronously on every landing (`schedule(); flush()`) at the #4308 head, so a served pass adds no flush per landing. Its trailing `flush()` is a no-op after a value landing. Tests: in served-source-landing, a write before the flush and an effect's write in the landing's flush both give the written value with 1 ask. Both fail on 2b2990a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * perf(solid): golf the served-pass client code This changes no behaviour. Client bytes over next go from about +130 B to +58 B minified. - A hydrated `<Errored>`'s content run and a served pass are one window kind: `_claimOwner = ~clock`, a window over no owner for the flush round it opened in. `~` keeps it truthy when clock is 0. - That replaces both `NO_CLAIM` and `_servedAt`. The latch checks one thing, `_claimOwner !== ~clock`, and a later round of an arrival's flush (an effect's write) still computes. `isClaiming` reads a number as no owner, so nothing claims, as before. - `servedPass` is curried (`servedPass(f)(x)`), so the arrival thenable is `initP.then(servedPass(a), servedPass(b))`. - A resume clears the hydration flag once, before either branch. The fresh branch's own flush was redundant with the resume's trailing one. The outcome check reads `?.[0]` instead of `?.length`. The pre-flush stays on both paths. Removing it makes the write-before-landing test fail, and comparing only `_claimOwner` without the round makes the effect-write test fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid): a rejected fragment's fresh render claims what the server sent Hydration never asks again for an answer the server sent. When a streamed <Loading> fragment rejects with no outcome (no <Errored> on the server could draw the failure: the one above it was already on the wire, or there is none), the client still renders the fragment fresh, so the graph under the boundary is created. Now every read in that render claims its serialized record instead of asking again: - a read the server resolved takes its value; - a read whose record is an error throws that error (as the server serialized it: redacted unless markSafeError) to the nearest client boundary through the normal path, or is an uncaught error when there is none; - only a read with no record at all computes (a truncated stream's refs are dropped from the registry). How: a rejection (the server's, or the stream cut short) now resumes the boundary with 0 rather than false, and resumeBoundaryHydration runs every fresh render as a served pass (`servedPass(set, adopt)`), adopting (`_hydratingValue`) only after a rejection. A superseded render and an asset failure do not adopt. servedPass resets its state in a `finally`, because an uncaught error leaves the flush by throwing. Recovery is a real input change or a client boundary's reset(), as for any hydrated failure (#4273). Changed expectations: async-memo-element-4242.spec.tsx, uncontained variants and their `-reference` twins (the generic "server render error at this position" path, so both change together; they still compare equal): - runs: [] for the jsx shape. `outer` adopts the server's error and never runs. (The loaded-mode check used to expect the client to compute the element: "jsx-41Count: 1".) The issue shape keeps ["inner"]. That run is the fixture's own: the dry run's continuation escapes the mock Promise through a real timer and builds the inner slot, whose memo has no owner id to adopt by. - halted: true, done: false (were false/true in loaded mode). The adopted error has no client boundary to reach (a <Loading> does not catch), so it is uncaught, as on any client render. The reactive system halts, and the halted resume never completes hydration. - text "", afterClick undefined (were "jsx-41Count: 1"/"Count: 1", and "nested-41"). The element came from the errored memo, so it never renders and there is no counter to click. - shown ["outer", ""] streamed, [""] loaded: the fallback, then nothing. client-hydration.spec.ts, "rejected _fr resumes without hydrating serialized children" -> "rejected _fr resumes fresh, adopting serialized children": memo() 0 -> 7. The fresh DOM stays, but the serialized 7 is the server's answer, so the client uses it rather than computing 0. client-recovery-record.spec.ts (three tests): b.memo() 0 -> 7, for the same reason. The recovery records are unchanged. New: served-error-no-reask.spec.tsx with two parity-harness scenarios. - nested-rejection-no-outcome: a shell <Errored> over a streamed outer <Loading> (a resolved read, a counter) and an inner <Loading> whose read fails on the server beside a read the server resolved. The inner fragment rejects with no outcome. The fresh render shows the server's error in the shell Errored, with 0 asks for either read. reset() asks once, and the recreated graph is live (the counter clicks). - served-error-reread: a served error mid-hydration (a slow boundary still pending) stands with no ask. reset() is a real retry: one ask, and it recovers while hydration still runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(web): a fresh render adopts only what the server answered, by aligned ids The previous commit made a rejected fragment's fresh render adopt what the server sent by id. That exposed two wrong-data cases. Abandoned records. When a fragment fails on the server with nested reads still pending, abandonSubtree settles those records to release seroval, and they reached the client as `undefined`. The fresh render adopted that `undefined` as the server's answer. The server now also unregisters them: a record not yet on the wire is dropped from the stub batch, and one already registered gets a `delete _$HY.r[id]` document task, queued ahead of the `_fr` rejection that triggers the fresh render. The read then has no record and computes. Frame sinks are untouched, since they have no reader for these records. Server-only: 0 B on the client for this part. Id alignment. The server spends a child id on every element's hydration key. A non-claiming client render did not (getNextElement only read the key while claiming), so in a fresh render every owner after an element got a shifted id. It could miss its own record or take a sibling's. Now getNextElement consumes the key whenever hydration is on, claiming or not. A non-adopting fresh render runs with the flag off, so it is unchanged. Hydratable-compiled bundles only. Test: abandoned-sibling-computes (loaded and streamed). A <Loading> holds a read `a`, a read `d` after an element, and a nested <Loading> with a read `c` (50ms on the server). Its content throws on the server once `a` lands, a render error that no record carries. The fragment rejects with `c` still pending. The client shows "adc!an0tail": `a` and `d` are adopted with 0 asks, `c` computes once and gets its own value, and the counter clicks. Without the server change `c` shows "" (it adopts `undefined`). Without the key change `d` asks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * perf(solid): read the page's _$HY directly on the recover path A recover only runs while hydrating, so the guarded globalThis read is dead weight: -12 B minified on every hydrating and page scenario. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 * test(web): #4316's parked late case and twin warnings under adopted fresh renders With the fresh render adopting the server's records, the uncaught late variant halts (no client boundary, as on the server) and hydration never completes, as #4242's uncontained variants do. The parked twin parity now compares warnings too: the streamed key miss it excluded no longer happens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid,web): a rejected fragment's fresh render creates Dynamic tags and mounts Portals Review of #4319 found two regressions, both in a <Loading> whose server render threw. Both are caused by c376083: the adopting fresh render runs with `sharedConfig.hydrating` on (servedPass sets `_hydratingValue = adopt`, packages/solid/src/client/hydration.ts:605 at 57b6925) but claims nothing. Before it, a fresh render ran with hydration off. 1. <Dynamic component="p"> threw "Hydration Mismatch. Unable to find DOM nodes for hydration key", and the page halted. staticElement (packages/web/src/index.ts:323-330) branches on the bare flag and calls getNextElement() with no template. Off the claim, getNextElement can only create from a template, so it threw. That has been the case since before the key change too. Fix: pass the create thunk. The key is spent (ids stay aligned) and the element is created, like a compiled template. This is the only template-less getNextElement caller. getHydrationKey has no other client callers. 2. <Portal> never mounted. Its content memo is `ssrSource: "client"` (packages/web/src/index.ts:155), so with hydration on it goes through withHydrationGate (hydration.ts:1174-1178), which writes setHydrated(true) right after creation. hydrateSignalLike calls markTopLevelSnapshotScope (hydration.ts:1191, 327-335). In the fresh render no window owned the snapshot root, so it marked the page root as a snapshot scope. The root's scope is released only by the `hydrating` setter (hydration.ts:1557-1564), and servedPass bypasses the setter. The scope stayed open, the gate write stayed held, and the gate memo never re-ran (confirmed: it ran once, with hydrated() false). Fix: an adopting servedPass sets `_snapshotRootOwner` for its duration, so the render opens no snapshot scope. That is a client render, as before c376083. The rest of the gated nodes (the Portal anchor memo, index.ts:225) are fixed by the same change. Sweep of the other sites that branch on the bare flag during such a render. None breaks: - clientOnly (index.ts:458/468) shows its fallback and swaps at hydration end; - lazy (component.ts:188/276), the dynamic factory warm-up (dynamic.ts:173) and eager list mapping (flow.ts:106) are harmless or keep ids aligned; - reclaimRegion (client.ts:1484) only acts on a connected parent whose tracked nodes are detached, which is the cleared fallback region here. Nits: - resumeBoundaryHydration runs checkHydrationComplete in a finally. An adopted error with no client boundary throws out of the render, but the boundary has resumed, so hydration completes. Updated: #4242's uncontained variants and twins, and #4316's late case (16cec07), now expect done true. halted stays true. - getNextElement: the reviewer's trim (node read only while claiming, one `!node` test). Tests (served-error-no-reask.spec.tsx; new scenarios rejected-fragment-dynamic and rejected-fragment-portal, a server-only render throw after the shell): loaded and streamed, the text is "adyn!atail" / "a!aportaltail", hydration is done, and there are no console errors. Before the fixes: "tail" (halted) and "a!atail" (no portal). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * perf(solid,web): one try/finally for a served pass that throws servedPass's `finally` now runs checkHydrationComplete itself, after resetting the pass state. resumeBoundaryHydration drops its own try/finally: a boundary's fresh render is that resume, and its uncaught error still completes hydration. Everywhere else a served pass's check is a no-op: a landing comes when hydration is done, or while boundaries are still pending. getNextElement reads isHydrating() inline instead of through a variable (the dev mismatch warning calls it again, dev only). Behaviour is unchanged. It saves 8 B minified on every hydrating bundle (5 B in the compiled ones). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): raise the hydrating + store cap for #4319 over #4314 Size-Exception accepted by the maintainer (2026-10-27): +34 B minified over the recorded size on "app: hydrating + every store primitive family". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(solid,web): a rejected fragment's fresh render renders NoHydration and gates its stylesheets The adopting fresh render (#4319) runs with sharedConfig.hydrating on but claims nothing. Two readers took the bare flag for "claiming server DOM": - NoHydration returned nothing, so its children vanished ("a!atail" instead of "anh!atail"). - gateHeadResource skipped the stylesheet reveal gate, so content the server never drew revealed before its sheet loaded. Both now read isHydrating(), the claiming check. 0 B on every size scenario. The other bare readers stay: For's eager map, createUniqueId, the store, memo and signal wrappers and the Loading wrapper spend or adopt ids the server spent; clientOnly and Portal's tree marker defer through the hydration-end gate; the lazy lookup reads the module cache; the head flush and reclaimRegion never run inside the pass or are guarded on connectivity. New scenarios pin NoHydration, clientOnly, For, a nested Loading and a stylesheet inside a rejected fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #4306
Before: when an inner
<Loading>fails while its outer<Loading>is still pending, and the<Errored>fallback has an async hole, the server splices nothing over the inner placeholder and resolves the fragment as success. The client then claims the Errored region against nothing: it rendersoutertailand logs two hydration warnings. Without the outer<Loading>, the failure settled at once as an empty rejection and the client rendered the fallback fresh.After: a failed
<Errored>whose fallback has async holes waits for them, then sends the finished fallback and its error record for the client to claim. This happens whether or not the fragment is parked, per D-18 (a fallback that reads pending data: the old content, or the parent's fallback on first load, stays until the fallback is ready). The maintainer chose this "always wait" behaviour. The issue's shape hydrates toouterfailed: no user 7 HINTtailwith no warnings, in both loaded and streamed order.How:
renderOutcome(non-server-component path) returns a function when the fallback has async holes. The function waits for the holes, re-runs them with the render context restored, and returns the markup, serializing the error only then.errorOutcomecalls it on the template-hole error path.failFragment/finalizeErrorcan return a promise, which the resume loop awaits so the reveal-group release still followsdone().errored-loading-parked-rejection-async-fallback-4306and…-late-async-fallback-4306, pluserrored-parked-async-outcome-4306.spec.tsx.Known, not fixed here: a client that hydrates mid-stream re-asks
useronce and briefly shows the inner loading fallback again after the failure. This is the existing "render fresh on the_frrejection" path, whichnextalready shows. Fixing it needs the client to claim the streamed fallback, which costs client bytes.Size: 0 B brotli and minified in every scenario against
next.🤖 Generated with Claude Code
https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2