Skip to content

fix(signals): refresh() lifts a stale manual-write mask inside a transaction - #3029

Merged
ryansolid merged 1 commit into
solidjs:nextfrom
brenelz:fix/refresh-in-action-3026
Aug 21, 2026
Merged

fix(signals): refresh() lifts a stale manual-write mask inside a transaction#3029
ryansolid merged 1 commit into
solidjs:nextfrom
brenelz:fix/refresh-in-action-3026

Conversation

@brenelz

@brenelz brenelz commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #3026

The bug

refresh(store) on a derived store (createStore(fn, init)) never re-ran the source function when called inside an action() — but only when a setStore had happened earlier in the same transaction (in the issue repro, the setStore right after act1() is absorbed into the still-ambient action transition).

Root cause: the derived-store setter calls suppressComputedRecompute, which sets REACTIVE_MANUAL_WRITE so 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 any setStore early in an action swallowed every later refresh() for the rest of the transaction. That's why deferring the refresh in a setTimeout (after commit) worked.

The fix

suppressComputedRecompute now stamps the current clock tick on the node (_manualWriteTime), and refresh() distinguishes the two cases:

  • Same synchronous tick: the manual write still wins over the refresh, in both orders — the pinned Solid v2.0.0-beta.10 batched parent & derived signals manual update is neglected for derived signals #2692 contract ("manual memo writes win over refresh within the same tick", createMemo.test.ts) is unchanged.
  • Mask stamped in an earlier tick (only possible when a transaction is holding the drain open): the explicit refresh lifts the mask, the source re-runs, and its result reconciles over the transaction draft at commit — one atomic reveal.

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), and store/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

@changeset-bot

changeset-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d76914a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@solidjs/signals Patch
test-integration Patch
solid-js Patch
babel-preset-solid Patch
@solidjs/web Patch
@solidjs/html Patch
@solidjs/h Patch
@solidjs/universal Patch
@solidjs/element Patch

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
brenelz force-pushed the fix/refresh-in-action-3026 branch from 05b7263 to d76914a Compare August 21, 2026 02:02
@codspeed-hq

codspeed-hq Bot commented Aug 21, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 24.3%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 126 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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)

Open in CodSpeed

@ryansolid
ryansolid merged commit 8890092 into solidjs:next Aug 21, 2026
6 of 7 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants