Skip to content

fix(signals): awaited refresh returns the source, not the caller override (#3895) - #3931

Merged
ryansolid merged 4 commits into
nextfrom
fix/3895-refresh-optimistic-authority
Oct 8, 2026
Merged

ryansolid merged 4 commits into
nextfrom
fix/3895-refresh-optimistic-authority

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

Awaiting refresh() of an optimistic accessor inside the action that wrote the override returned the caller's guess. The refresh waiter reads the target through laneRead, and a written guess stands in for that node's own in-flight re-ask, so the promise settled with the overlay while the source flight was still pending.

A fresh-pull of a guessed node now parks until that flight lands or rejects, then serves the truth staged beneath the guess. until() is unchanged (it has no fresh-pull bit). The waiter's root and the action transaction it is created in stay, so a refresh of the underlying source still delivers the staged landing.

Fixes #3895

Public API

None

How did you test this change?

From the worktree, after pnpm install:

  • pnpm --filter @solidjs/signals exec vitest run tests/refresh-optimistic-authority-3895.test.ts failed on current next before the fix (expected 99 to be 2).
  • After the fix, pnpm --filter @solidjs/signals exec vitest run tests/refresh-optimistic-authority-3895.test.ts tests/refresh-await.test.ts tests/until.test.ts tests/createOptimistic.test.ts — 4 files, 101 tests, all passed.
  • pnpm --filter @solidjs/signals test — 5056 passed. The 27 failures are all tests/dist-artifacts.test.ts (ENOENT / Cannot find module for dist/), because this worktree has no package build. Signals unit tests import source. refresh()'s createRoot waiter from [2.0 next, regressed after rc.13] Production refresh remains pending after a settled computation (production build) #3888 is untouched.

The new tests cover the issue repro (source answers 2, override is 99), a re-ask that lands a new value (5, not the pre-refresh commit and not the override), a re-ask that confirms the override, and a rejected re-ask.

Made with Cursor

…ride (#3895)

A refresh waiter reads its target through laneRead. A written guess stands
in for that node's own in-flight re-ask, so awaiting refresh() of the
optimistic accessor resolved with the caller's override while the source
flight was still pending. The screen corrected when the source landed; the
promise had already settled.

A fresh-pull of a guessed node now parks until that flight lands or rejects,
then serves the truth staged beneath the guess. until() has no fresh-pull
bit, so it still sees the base the guess covers. The waiter's root and the
action transaction it was created in stay, which is what delivers a staged
landing when refresh targets the underlying source.

Co-authored-by: Grok via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c25c268

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

This PR includes changesets to release 12 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch
todos-server-example 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

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.44 KB 0 B 0 B +2 B 7.45 KB ✅
signals: + createStore 14.69 KB 0 B 0 B +11 B 14.70 KB ✅
signals: + isPending/latest 9.63 KB 0 B 0 B −4 B 9.65 KB ✅
app: render + one signal (the simple-app floor) 9.92 KB 0 B 0 B +2 B 9.93 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.87 KB 0 B 0 B 0 B 17.91 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.16 KB 0 B 0 B +63 B 29.19 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.93 KB 0 B 0 B 0 B 12.96 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.51 KB 0 B 0 B 0 B 14.53 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.83 KB 0 B 0 B −1 B 28.89 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.12 KB 0 B 0 B +2 B 10.13 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.20 KB 0 B 0 B +64 B 25.24 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.17 KB 0 B 0 B +9 B 31.17 KB ⚠️ over by 2 B, 11 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.38 KB 0 B 0 B +14 B 11.39 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.18 KB 0 B 0 B −1 B 34.22 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.25 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.85 KB 0 B 0 B −7 B 37.94 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.24 KB, wire.js 0.93 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.40 KB 0 B 0 B −1 B 35.47 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.24 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.92 KB 0 B 0 B −7 B 40.95 KB ✅ eager (counted): web.js 22.02 KB; assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.24 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.52 KB 0 B 0 B −1 B 41.56 KB ✅ assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, serverForms.js 3.58 KB, trace.js 8.28 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.28 KB 0 B 0 B 0 B 47.30 KB ✅ eager (counted): client.js 27.87 KB; assets.js 0.78 KB, bind.js 1.86 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, serverForms.js 3.31 KB, trace.js 8.23 KB, wire.js 0.94 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.40 KB 0 B 0 B +4 B 20.42 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 2 B; minified 99,681 B vs 99,672 B recorded with the cap (+9 B) — 11 B of the 20 B minified allowance left; +0 B minified over this PR's base

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

@coveralls

coveralls commented Oct 8, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37859863127

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.1%) to 76.593%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 1 coverage regression across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
packages/solid/src/client/component.ts 1 80.0%

Coverage Stats

Coverage Status
Relevant Lines: 1243
Covered Lines: 1011
Line Coverage: 81.34%
Relevant Branches: 970
Covered Branches: 684
Branch Coverage: 70.52%
Branches in Coverage %: Yes
Coverage Strength: 28.19 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 10.48%

⚠️ 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

⚡ 2 improved benchmarks
✅ 193 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ projection derive: write one NESTED field (reference) 2.4 ms 2.1 ms +14.15%
⚡ memo + sync render effect only (reference) 32.5 ms 30.4 ms +6.93%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/3895-refresh-optimistic-authority (c25c268) with next (0c4bff7)

Open in CodSpeed

ryansolid and others added 2 commits October 8, 2026 16:20
laneRead ships in every isPending/latest bundle. Only refresh() needs to
ignore a guess while that node's flight is up, so the park lives in the
waiter and pages that never call refresh stop paying for it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit c689a9a into next Oct 8, 2026
7 checks passed
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