fix(signals): refresh() lifts a stale manual-write mask inside a transaction - #3029
Merged
Merged
Conversation
🦋 Changeset detectedLatest commit: d76914a The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 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 |
…saction (solidjs#3026) A derived store's setter masks the recompute for the tick (REACTIVE_MANUAL_WRITE) so a manual write wins over a queued recompute (solidjs#2692). The mask only clears at the pending-node drain, which inside an action's transaction is the whole action — so a setStore absorbed into the transaction made every later refresh() in that action a silent no-op: the store source never re-ran. Stamp the clock on the mask (suppressComputedRecompute) and let refresh() distinguish the two cases: a manual write in the same synchronous tick still wins in both orders (the solidjs#2692 contract, unchanged), while a mask stamped in an earlier tick — only possible when a transaction holds the drain open — is lifted by an explicit refresh, which re-runs the source and reconciles over the transaction draft at commit. No quiet re-ask (REASK) classification when lifting: the batch carries a real value change. Fixes solidjs#3026 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brenelz
force-pushed
the
fix/refresh-in-action-3026
branch
from
August 21, 2026 02:02
05b7263 to
d76914a
Compare
Merging this PR will degrade performance by 24.3%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | omit |
235.8 µs | 311.6 µs | -24.3% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing brenelz:fix/refresh-in-action-3026 (d76914a) with next (77d1348)
ryansolid
added a commit
that referenced
this pull request
Aug 21, 2026
… the redundant in-heap gate Offsets #3029/#3030 landing on always-retained code: the child loop's heap gate duplicated deleteFromHeap's own guard, and its dep-unlink block was unobserved()'s body. Floor scenarios back under budget (10000/10000, 9198/9200 brotli). Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3026
The bug
refresh(store)on a derived store (createStore(fn, init)) never re-ran the source function when called inside anaction()— but only when asetStorehad happened earlier in the same transaction (in the issue repro, thesetStoreright afteract1()is absorbed into the still-ambient action transition).Root cause: the derived-store setter calls
suppressComputedRecompute, which setsREACTIVE_MANUAL_WRITEso a manual write wins over a queued recompute for the tick (#2692 / core R31). That mask is only cleared by the pending-node drain (commitPendingNode) — which inside a transaction doesn't run until the action commits.refresh()silently bailed on the mask, so anysetStoreearly in an action swallowed every laterrefresh()for the rest of the transaction. That's why deferring the refresh in asetTimeout(after commit) worked.The fix
suppressComputedRecomputenow stamps the current clock tick on the node (_manualWriteTime), andrefresh()distinguishes the two cases:When lifting the mask, no
REACTIVE_REASK(quiet re-ask) mark is set, since the batch carries a real manual value change.Tests
New tests in
action.test.ts("refresh(store) inside an action") cover the issue shape and the re-fetch shape (fresh object per derive — reconciled result wins over the draft at commit), andstore/createStore.test.ts("derived store manual writes") pins both #2692 precedence directions (same-tick setStore-then-refresh and refresh-then-setStore both keep the manual write) plus the cross-tick refresh. Full solid-signals and solid suites pass with no regressions.One note on the issue's exact repro: the derive there returns the same object identity the store already adopted as its raw state, so the re-run's reconcile is an identity no-op (owned-raw model, pre-existing behavior). The source function re-running — the reported symptom — is fixed; realistic re-fetch derives that return fresh data get the full expected reconcile.
🤖 Generated with Claude Code