Conversation
…lidjs#3743) The fold's presence loops (notifyFold, notifyFoldTail) wrote every observed `in` node without comparing old and new presence, while the value loop already diffs. setSignal joins the node's holding transaction before its equality gate rejects the repeat, so a reconcile() outside an action that had deleted an observed key entangled the whole tick with that action: an unrelated `a.value` stayed stale until the action settled. The presence loops now write only when `key in old` differs from `key in neu`; `old` is the view the nodes were last told (solidjs#3296), so a real presence change still notifies and still proposes on a held node.
applyAdopt materializes a prototype-overlay pending backing before taking its diff base. A nested object wide enough to draft as an overlay (more than 32 keys, owned) read a key the draft had deleted as still present through the overlay's prototype, so the fold's presence skip fired on a real deletion: an unrelated value stayed stale until the action settled, and a reconcile restoring the key never proposed on the held node. The presence loops in notifyFold and notifyFoldTail write unconditionally when the old base is a live store proxy (a chained backing). `key in old` there reads the inner store's current state, not what this target's presence node was last told, so a projection adopting away from a chained store and then back to a key the inner store had deleted left the node stale and skipped the notification. The tests add the wide-object (overlay) versions of both solidjs#3743 scenarios and the chained adoption case.
🦋 Changeset detectedLatest commit: 9c491aa The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 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 |
Merging this PR will not alter performance
Comparing Footnotes
|
|
) Port of #3744 (brenelz) onto L2 (#3774). The fold's presence loop (`notifyFoldTail`) wrote every observed `in` node with `key in neu`; `setSignal` joins a held node's transaction before its equality gate (A34 (1): a write to a held node is a proposal, the same value or another), so a `reconcile()` outside an action that had deleted an observed key — the snapshot leaving it absent too — made the whole mainline tick the action's: an unrelated `a.value` stayed stale until the action settled. Presence is now diffed `old` -> `neu` like the leaves (the fold's contract, #3296: the view the nodes were last told); a live chained `old` (a store proxy) is written unconditionally as before. `notifyFold` shares the tail. `applyAdopt` materializes a nested overlay draft before taking its diff base (`adoptPB` did so after it was taken): through the overlay's prototype a deleted key still read as present, so the presence skip fired on a real deletion, and a reconcile restoring the key never re-proposed on the held leaf, which committed the draft's `undefined` beside a backing that had the key. Two L2 fixes the diff uncovered, pinned by the existing #2719/#3164 and Q-D twin tests and a new leaf-only reader case: a container carrying an arrangement guess is told of an arrangement change whether or not anything subscribes to it (`notifyFoldTail`/`notifyWrites`) — for a guessed container the write is the landing that judges the guess (Q-D, plan sec. 39), and before only the unconditional presence write reached the lane, so a newer question's rows landing beneath an optimistic push published beside it for a reader of leaves alone; and `_laneRebase` no longer re-stages a slot whose row the committed backing already shows by key, nor an unchanged `length` (a spurious identical frame). Signals 4900 / 0 / 3 expected fails (+9 pins: 8 green, one `it.fails` for the open setter-path case — `t.wk` is cumulative, not per-setter), solid 819 / 0, web 1132 / 1411 / 275, 0 failures. Size (br): + createStore 14517 -> 14523, every store family 28770 -> 28765, page base 44017 -> 44028, page live 47656 -> 47668 (minified -97..-100 B each); every scenario within its cap. Co-authored-by: Brenley Dueck <brenleydueck@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The port of #3744 measures 44,031 B brotli on the base server-components page against next @ 1a3f87f's 44,029 (+2 B, 1 B over the cap) while the same bundle is -97 B minified: brotli layout on the 150 KB bundle, not code. Cap set at measured + 10 B rounded up to 0.01 KB; dated note in scenarios.js beside the page-base notes. Size-Exception: page: base server components 44,029 -> 44,031 B (cap 44.03 -> 44.05 KB) — presence diff in store folds (#3743); source −97 B minified; accepted by the maintainer (2026-10-04) Co-authored-by: Cursor <cursoragent@cursor.com>
, supersedes #3744) (#3791) * fix(signals): a fold repeats no unchanged presence to a held node (#3743) Port of #3744 (brenelz) onto L2 (#3774). The fold's presence loop (`notifyFoldTail`) wrote every observed `in` node with `key in neu`; `setSignal` joins a held node's transaction before its equality gate (A34 (1): a write to a held node is a proposal, the same value or another), so a `reconcile()` outside an action that had deleted an observed key — the snapshot leaving it absent too — made the whole mainline tick the action's: an unrelated `a.value` stayed stale until the action settled. Presence is now diffed `old` -> `neu` like the leaves (the fold's contract, #3296: the view the nodes were last told); a live chained `old` (a store proxy) is written unconditionally as before. `notifyFold` shares the tail. `applyAdopt` materializes a nested overlay draft before taking its diff base (`adoptPB` did so after it was taken): through the overlay's prototype a deleted key still read as present, so the presence skip fired on a real deletion, and a reconcile restoring the key never re-proposed on the held leaf, which committed the draft's `undefined` beside a backing that had the key. Two L2 fixes the diff uncovered, pinned by the existing #2719/#3164 and Q-D twin tests and a new leaf-only reader case: a container carrying an arrangement guess is told of an arrangement change whether or not anything subscribes to it (`notifyFoldTail`/`notifyWrites`) — for a guessed container the write is the landing that judges the guess (Q-D, plan sec. 39), and before only the unconditional presence write reached the lane, so a newer question's rows landing beneath an optimistic push published beside it for a reader of leaves alone; and `_laneRebase` no longer re-stages a slot whose row the committed backing already shows by key, nor an unchanged `length` (a spurious identical frame). Signals 4900 / 0 / 3 expected fails (+9 pins: 8 green, one `it.fails` for the open setter-path case — `t.wk` is cumulative, not per-setter), solid 819 / 0, web 1132 / 1411 / 275, 0 failures. Size (br): + createStore 14517 -> 14523, every store family 28770 -> 28765, page base 44017 -> 44028, page live 47656 -> 47668 (minified -97..-100 B each); every scenario within its cap. Co-authored-by: Brenley Dueck <brenleydueck@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> * size: page base cap 44.03 -> 44.05 KB for #3743's fold presence diff The port of #3744 measures 44,031 B brotli on the base server-components page against next @ 1a3f87f's 44,029 (+2 B, 1 B over the cap) while the same bundle is -97 B minified: brotli layout on the 150 KB bundle, not code. Cap set at measured + 10 B rounded up to 0.01 KB; dated note in scenarios.js beside the page-base notes. Size-Exception: page: base server components 44,029 -> 44,031 B (cap 44.03 -> 44.05 KB) — presence diff in store folds (#3743); source −97 B minified; accepted by the maintainer (2026-10-04) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Brenley Dueck <brenleydueck@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
Fixes #3743
What was broken
A
reconcile()outside a pending action held unrelated updates (a.valuestayed stale) when a separateinexpression observed a key that the action had deleted and the incoming snapshot also left out.Root cause
The presence loops in
notifyFoldandnotifyFoldTailwrote every observedinnode without comparing old and new presence.setSignaljoins the node's holding transaction before its equality gate rejects the repeated value, so the reconcile was tied to the deleting action.What changed
key in olddiffers fromkey in neu, matching the value loop. Whenoldis a live chained store proxy, they still write unconditionally, because the proxy reflects the inner store rather than what the node was last told.applyAdoptmaterializes a prototype-overlay pending backing before using it as the diff base. Without this, a key deleted in a wide (more than 32 keys) owned draft still reads as present.A real presence change still notifies and still proposes on a held node, so deleting a prop override can still select another merge source.
Verification
cd packages/signals && npx vitest run tests/store/unchanged-presence-no-hold-3743.test.ts: 5 tests. The issue scenario, its wide-object version and the chained-projection case fail without the fix.cd packages/signals && npx vitest run: all pass.cd packages/solid && npx vitest run: 819 passed.cd packages/web && npx vitest run --config vite.config.mjs,vite.config.hydrate.mjsandvite.config.server.mjs: no new failures. One server failure inserver-functions-adapter-request.spec.tsxis pre-existing onnext.Left open
The setter-path case from the issue comment (
notifyWritesre-notifying keys kept in the cumulativewk) is not addressed here.