Repository navigation
fix(web): a deferred hole keeps its server component's claims arming - #4043
Conversation
A server component arms handler-position markers (`ctx.claims`) on a context that is current only for its synchronous render. A hole that escalates there is retried by whatever boundary resumes it — on the document face, often one outside the component — so its final markup rendered without the arming and dropped every `_s:on:click`. The chat thread's Copy buttons shipped unmarked and were bound only because the end-of-response sweep, which re-runs under the mint-time context, re-sent the whole message list with the markers. The retry wrapper now carries the escalation's claims and arms them on a context derived from the resuming one, so that boundary's buffering and serialization stay its own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SzJPKYTStjk5cEs9VDKNce
🦋 Changeset detectedLatest commit: 1a2faa5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 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 |
A deferred retry now carries its server component's claims arming, so buildAsyncWrap's retry wrapper grows renderToString by 34 B brotli (155 B minified). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SzJPKYTStjk5cEs9VDKNce
Size (brotli, eager entry chunk)
|
|
The Size Generated by Claude Code |
Coverage Report for CI Build 38540829579Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage remained the same at 77.515%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 5.02%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | dbmon shallow full tick |
52.9 ms | 50.4 ms | +5.02% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/project-thread-kunkh4 (1a2faa5) with next (a4b3391)
|
CodSpeed reports two regressions on acffd02, and I don't think they come from this PR:
Both benchmarks run Generated by Claude Code |
… floor raise Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SzJPKYTStjk5cEs9VDKNce
Re-measures the renderToString floor on top of #4043: 20,570 B (72,373 minified), combined cap 20.58 KB as the maintainer accepted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KRieFbP8sapvTBygyVm3mg
Requested by Ryan · project thread
Summary
Before: the chat example's thread page shipped its Copy buttons as
<button class="copy-code" type="button">, with no_s:on:clickmarker, so the client had nothing to bind. Copy only worked because the end-of-response sweep re-sent the whole message list oversc:livewith the markers added, so every load sent the thread twice.After: the markup the client adopts already carries
_s:on:click="codeBlock:onCopy" _hk, and no hole op is sent. On a six-exchange chat thread the page drops from 20.1 KB to 13.9 KB.The cause is that
frameTransformDirectResultarmsctx.claimson a context that is current only during the component's synchronous render. The message list reads async data, so its hole escalates. The retry is then re-pulled by a boundary outside the component, under that boundary's context, which has noclaims. The compiledctx.claims ? ssrClaim(…) : ""guard therefore emitted nothing. The sweep got it right only becauseswept()re-runs a hole under its mint-time context.buildAsyncWraprestores the hole's owner on retry but did not restore its arming.How:
buildAsyncWrapcaptures the escalation context'sclaims. When the resuming context doesn't carry the same arming, the retry setsclaimson a context derived from the resuming one, so that boundary's buffering and serialization stay its own. Restoring the old context wholesale would be wrong, because the retry may be streaming inside a different boundary.Size: the
renderToStringfloor grows by 155 B minified (+34 B brotli: 20,497 B to 20,531 B). That is 21 B over its frozen 20.51 KB cap, so the cap is raised to 20.55 KB. The maintainer accepted this together with #4055's growth on the same floor, under one combined cap of about 20.58 KB. Whichever of the two merges second re-measures on top of the first and sets the real number.Size-Exception: renderToString floor 20.51 -> 20.55 KB (+34 B brotli, +155 B minified) so a deferred hole's retry keeps its server component's claims arming; accepted by the maintainer together with #4055.
How did you test this change?
frame-binding-slots.spec.tsxwith the chat shape: a server component whose list reads an async memo, inside a<Loading>outside the component. It asserts that the deferred markup has the marker and that noholeop is sent. Before the fix it failed: the template had<button class="copy">and aholeop re-sent the list with the marker.packages/web: the three Vitest configs pass, exceptserver-functions-adapter-request.spec.tsx› "keeps a rebuild failure's own error", which also fails onnext.examples/chatand rendered a seeded thread through the builthandleRequest: all 6 Copy buttons are marked in the markup, 0 are unmarked, and noholeop is sent (only the composer's slot records).scripts/size:node size.mjspasses for renderToString, andcheck-floor-caps.mjs origin/nextpasses with the Size-Exception line.🤖 Generated with Claude Code
https://claude.ai/code/session_01SzJPKYTStjk5cEs9VDKNce