Repository navigation
fix(signals): latest() and isPending() in a hydration claim read the server's snapshot (#4570) - #4581
Conversation
…server's snapshot (#4570) A client write during the hydration window, while a streamed boundary is still out, re-arms a <Loading on> nested in it. When the server's answer streams in, the boundary resumes in a snapshot scope: a plain read is served the value the server rendered with, but latest() and isPending() read through the verdict window, which did not serve it. latest(a1) beside a1() saw the client's flight (a1:1, no value yet), threw, and put the inner boundary in a fallback the server never rendered, claimed against nothing: "Hydration key miss", a detached <p>F1</p>, and F1 beside the snapshot's a1=10 (F-1; D-16 says that re-arm's fallback never shows). verdictValue now serves the snapshot to a reader in a snapshot scope, as read() does, marking it stale so it replays at the scope's release. The code is in the verdict engine: apps without latest/isPending pay nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…indow (#4570) The verdict window's copy of read()'s snapshot serve cost the live pages 125-159 B minified. Both sites now call one helper, servedSnapshot, behind the same inline guard, so builds that never hydrate still fold both away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
…paring (#4570) A verdict reader served the server's snapshot in a hydration claim is marked stale unconditionally; read() already compares before re-running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
|
The red Generated by Claude Code |
🦋 Changeset detectedLatest commit: b61bf62 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 |
…hout comparing (#4570)" The unconditional re-run makes a verdict reader in a claim emit twice at release (create-deferred-hydration.spec.ts, D-40). Keep the compare. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE
Size (brotli, eager entry chunk)
|
Coverage Report for CI Build 41293580675Coverage remained the same at 78.134%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will degrade performance by 1.02%1 warning
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
|
The red CodSpeed check on b61bf62 is the "arrow in template" SSR bench (−11.6%). It's a known noisy bench in this repo, and it swings like this on PRs that don't touch the server render path. This PR only changes the client hydration verdict path, and every signals bench is untouched or improved, so I'm not chasing it. Generated by Claude Code |
Fixes #4570.
Before: a client write during the hydration window can re-arm a
<Loading on>nested in a streamed<Loading>while the server's answer is still out. When that answer streamed in, the hydrated page showed the inner fallback beside the server's answer (F1 a1=10). The fallback was claimed against server markup that never contained it, so dev logged "Hydration key miss" and the<p>F1</p>was created detached.After: the boundary resumes on the server's frame,
La1=10 a1=10, and holds it until the client's own question lands (La1=11 a1=11). No fallback is shown, nothing is claimed that the server didn't render, and nothing the server answered is asked again.Rule.
a1) is also read outside the inner boundary, so the re-arm's fallback is never shown.latest(a1)'s world besidea1()'s.This is the same contract as the write-before-resume pin: a boundary resumes against the server snapshot, then catches up.
Cause.
read()serves each reader the value the server rendered with.latest()andisPending()read through the verdict window (verdictValue), which returns before that serve. Solatest(a1)saw the client's flighta1:1, which has no value yet, and threw.a1()read the same app hydrates cleanly.How.
verdictValuenow serves the snapshot to a reader in a snapshot scope, exactly asread()does.REACTIVE_SNAPSHOT_STALEwhen the live value differs, so the reader replays at the scope's release.isPendinganswers for the server's frame: nothing in flight.Size and perf. The code is in the verdict engine (verdict.ts). Apps that don't use
latestorisPendingpay 0 B, including hello world and the core floor. Rows that install the engine grow by the one branch; CI's size report has the numbers. The branch runs only while hydration capture is active and the reader is in a snapshot scope, so nothing changes outside hydration.Tests.
packages/web/test/hydration/hydration-write-rearm-4570.spec.tsx, with its harness and server generator, replays the real streamed chunks in two shapes:sharedis the issue. It failed withF1a1=10plus the key miss before the fix.ownis a re-arm whose data nothing outside reads. There the fallback shows now (D-15) and is created fresh. It already passed and is kept as a pin.packages/signals/tests/latest-snapshot-scope-4570.test.ts: in a snapshot scope,latest(x),x()andisPending(x)agree on the snapshot until release. It failed before as[1, 5, false].latestsignals tests, and the new specs under both compilers. CI runs the rest.Generated by Claude Code