Repository navigation
fix(solid): Show's narrowed accessor reads the screen during a closing hold (#4354) - #4359
Conversation
…g hold (#4354) Non-keyed Show judged staleness from a `shown` flag the selection memo set on every run. When a hold kept the branch on screen, the selection had already re-run in the held world and set the flag false, so an unrelated write that re-ran an on-screen binding threw the stale-value error and halted. The guard now reads `condition()` tracked, as Match's guard does: the reading binding is a frame reader and sees the screen's truthiness, while a read after unmount still sees the committed falsy and throws. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
🦋 Changeset detectedLatest commit: 2982c08 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 40676203596Coverage decreased (-0.02%) to 78.709%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
Brings in #4347, #4350-#4353, #4356, #4357, #4359 and #4360. The one conflict was in the store-app entry of scripts/size/scenarios.js. Both ledgers are kept: next's two lines, then this branch's two. The cap is re-measured after the merge. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
… on a just-mounted branch (Show: #4359 regression) (#4648) (#4657) * fix(solid): a narrowed Show/Match accessor is live inside isPending() on a just-mounted branch (#4648) Since #4359 the non-keyed Show guard reads `condition()` tracked. Inside an `isPending()` thunk that read goes through the verdict path, which answers with the committed value of a condition staged this flush, so a branch that mounted in the flush its async condition landed in read as stale: the guard threw, `isPending` swallowed it, and nothing the thunk read after the accessor linked, so the verdict never flipped. Match's guard had the same shape. Show and Switch now remember the accessor their selection's own run handed out; that accessor is live whatever the frame. Any other accessor (a branch a held re-run has already closed, a read after unmount) falls back to the tracked condition read, so #4354 and #3948 keep their behaviour. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE * perf(solid): scope the #4648 live-accessor mark to the selection run Show and Switch keep one closure token per selection run instead of resetting and re-setting the accessor they last handed out, and keep the original ternary shape. Switch reuses its fresh selection tuple as the token. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE * fix(solid): limit the #4648 live-accessor skip to verdict windows The run token let the latest run's narrowed Show/Match accessor skip the stale guard in every frame, so a plain read while the branch's opening was held (a shown Loading re-asking on the condition) handed out the committed falsy instead of throwing, as it did on next. The skip now holds only while a latest()/isPending() window is open, read from the core's existing `verdict` dispatch slot (re-exported @internal; no change in verdict.ts). The token stays a fresh object rather than the condition value: an earlier run's closure can hold an equal value (true -> false -> true), and with that token its accessor would skip the guard inside a probe during a held reopen. Both cases are pinned. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE * perf(solid): narrowedError throws; Show/Match guards as short-circuits (#4648 review trim) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0117BoWNSSgtemsSYsaX2hXE --------- Co-authored-by: Claude <[email protected]>
Fixes #4354
Summary
Non-keyed
<Show>decided whether the narrowed accessor was stale from ashownflag (#3948). The selection memo set that flag every time it ran. Say a hold keeps a closing branch on screen:setOpen(false)is held by a<Loading>sibling's re-ask. The selection has already re-run in the held world and setshown = false. If an unrelated write (setFmt) then re-runs one of the on-screen branch's bindings, itsv()read throws "Attempting to access a stale value from …". Underrender()that shows up asREACTIVITY_HALTEDand the screen freezes.<Switch>/<Match>with the same shape does not throw.The fix makes Show's guard work like Match's: it reads
condition()tracked (if (!condition()) throw …) and drops theshownflag.latest()-selected first mount ([2.0 rc.14 / next]<Show when={latest(x)}>throws "stale value from<Show>" on first mount and halts reactivity #3948).untrack(condition): it handles neither case; both [2.0 rc.14 / next]<Show when={latest(x)}>throws "stale value from<Show>" on first mount and halts reactivity #3948 tests fail with it.The binding now also tracks
condition, the truthiness memo, which already exists. There are no new computations, effects or exports.conditiononly changes when truthiness flips, and when that happens the branch is torn down anyway. The keyed mode has no accessor and no guard, so it is unchanged.How did you test this change?
New tests, each of which fails on
nextin the non-keyed Show case and passes with this change:packages/solid/test/show-shown-flag-halt-4354.spec.tscovers four modes: Show, Show keyed, Switch/Match and Match keyed.<Show>/<Match>.ru:a,en:a,fr:a) and lands onfr:b.packages/web/test/show-held-close-binding-4354.spec.tsxruns the same shape as JSX underrender(), keyed and non-keyed, with${fmt()}:${v()}in one text binding.The existing #3948 tests (
show-latest-3948in solid and web) still pass.Suites (run after a clean
turbo build):uncommitted-input-overwritten, which also fails onnextlocally.server-functions-adapter-request"keeps a rebuild failure's own error…" andlone-spread-hydration-key-4240. Both also fail onnextlocally, and the server bundle doesn't contain this change.c03-hydration-done-counts-holdsflake.test-typespasses in solid and web.Size (
scripts/size, head vs base in the same environment): every scenario that bundles Show is −7 B minified. Brotli changes by −69 to +30 B, which is layout noise. Scenarios without Show are unchanged, including the hello-world floors.gate.mjspasses, and no caps or frozen floors move.Generated by Claude Code