Skip to content

fix(signals): a new async memo created over a hold lands its first answer into it (#3800) - #3881

Merged
ryansolid merged 2 commits into
nextfrom
fix/new-async-memo-held-3800
Oct 7, 2026
Merged

ryansolid merged 2 commits into
nextfrom
fix/new-async-memo-held-3800

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Fixes #3800. Thanks to @GabbeV for the minimal primitives-only repro and the two controls (a synchronous first result, and no flush() before the mount), which isolated the async first-result path.

What happened

count is written to 2 and held by an existing async derivation. After an explicit flush(), a new memo returning Promise.resolve(count()) reads the held 2. Its scheduled render effect logged child 2 while the parent was still at parent 1 1. The same memo returning count() synchronously stays held until the commit.

Root cause

  • A first pass that reads a held node is born held (A29, creation-time form). But the born-held arm in recompute runs only when the pass returns a value. An async first answer throws NotReadyError, so the arm never runs.
  • The seam then treats the node as a loading source, which no transaction holds (the 2.0.0-beta | The curious case of a deadlock #2937 rule, plan §15.2).
  • Its promise landed through a plain setSignal as mainline commit #0. The child's render effect published child 2 beside the held page.

The fix

This is the fix/create-time-holds encoding (91e474506), ported to current next and trimmed.

Maintainer clarification (2026-10-07): the #3869 ruling ("new memos created don't need to be a part of it") covers new memos that don't read async. A new memo that reads the held world is held, whether its first answer is a value or a promise.

Spec

Tests

Size

Local harness against next (dafad1db3); CI numbers follow in a comment.

Scenario Minified Brotli
app: render + one signal (hello world) +142 9878 → 9914 (well under 10,240)
signals: core floor +139 7382 → 7432
signals: + isPending/latest +135 9570 → 9634
signals: + createStore +145 14603 → 14667
other client app/page scenarios +136 to +149 −63 to +100
frames, server scenarios 0 0

Size-Exception: approved 2026-10-07 by the maintainer on the condition hello world stays under 10 KB.

Public API changes

None.

…swer into it (#3800)

A memo created while a transaction holds a value, whose first pass reads
that value and returns a promise, was a loading source the seam does not
hold (plan sec. 15.2), so its answer landed as mainline commit #0 and its
readers showed the held future beside the committed page. The same memo
with a synchronous first answer is born held (A29). The first pass now
records the transaction it was born into under the born-held arm's
conditions (a joined read, or a verdict lane's mount reading a staging),
and its first landing is staged into that transaction while it is live.
The hold never waits for the first load (the direction rule, #3820).

Maintainer clarification (2026-10-07): the #3869 ruling covers new memos
that don't read async; a new memo that reads the held world is held,
async first answer included.

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

changeset-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: dfac67d

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 7, 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.43 KB +50 B (+0.7%) +139 B 0 B 7.45 KB ✅
signals: + createStore 14.67 KB +64 B (+0.4%) +145 B 0 B 14.68 KB ✅
signals: + isPending/latest 9.63 KB +64 B (+0.7%) +135 B 0 B 9.65 KB ✅
app: render + one signal (the simple-app floor) 9.91 KB +36 B (+0.4%) +142 B 0 B 9.93 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.89 KB +52 B (+0.3%) +136 B 0 B 17.91 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.18 KB +100 B (+0.3%) +149 B 0 B 29.19 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.95 KB +82 B (+0.6%) +139 B 0 B 12.96 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.52 KB +67 B (+0.5%) +138 B 0 B 14.53 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.88 KB +72 B (+0.2%) +138 B 0 B 28.89 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.12 KB +50 B (+0.5%) +143 B 0 B 10.13 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.22 KB +35 B (+0.1%) +141 B 0 B 25.24 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.11 KB +26 B (+0.1%) +140 B 0 B 31.12 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.11 KB 0 B 0 B 0 B 11.13 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.84 KB −63 B (−0.2%) +146 B +146 B 33.92 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.19 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.58 KB +66 B (+0.2%) +140 B 0 B 37.59 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.19 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.12 KB +41 B (+0.1%) +146 B 0 B 35.13 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.17 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.65 KB +52 B (+0.1%) +139 B 0 B 40.66 KB ✅ eager (counted): web.js 22.02 KB; assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.19 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 46.01 KB +70 B (+0.2%) +141 B 0 B 46.02 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, trace.js 8.19 KB, wire.js 0.93 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.28 KB +80 B (+0.2%) +142 B 0 B 47.29 KB ✅ assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.24 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, trace.js 8.20 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 ✅

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).

… its hold (#3800)

Raises seventeen caps to CI brotli + 10 B with their recorded minified,
from Size run 37684843282 against next @ dafad1d — the frozen core,
simple-app, hydrating and live-page floors among them. Accepted by the
maintainer 2026-10-07 on the condition hello world stays under 10 KB
(CI: 9,914 B).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@coveralls

coveralls commented Oct 7, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37685484746

Coverage remained the same at 76.43%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1227
Covered Lines: 996
Line Coverage: 81.17%
Relevant Branches: 958
Covered Branches: 674
Branch Coverage: 70.35%
Branches in Coverage %: Yes
Coverage Strength: 28.38 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 2.73%

⚠️ 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 improved benchmark
❌ 1 regressed benchmark
✅ 186 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ memo + sync render effect only (reference) 27.7 ms 32.5 ms -14.76%
⚡ createStore setter: delete + set one root key (#3044 overlay) 2 ms 1.8 ms +10.99%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/new-async-memo-held-3800 (dfac67d) with next (dafad1d)

Open in CodSpeed

@ryansolid
ryansolid merged commit bc51ada into next Oct 7, 2026
7 of 8 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