Skip to content

docs(signals): async rules — the 2026-10-09/10 rulings in F/D - #3976

Merged
ryansolid merged 6 commits into
nextfrom
docs/async-fundamentals
Oct 11, 2026
Merged

ryansolid merged 6 commits into
nextfrom
docs/async-fundamentals

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

The async rules as reviewed 2026-10-09/10, carried into RULES-FUNDAMENTALS.md / RULES-DERIVED.md (the rules are the artifact; no separate rulings doc).

  • F-2 / D-2 / D-6: first loads in a branch a change mounts count; a memo born held is loading. <Loading> is how a change escapes the hold. The direction rule's time half (drafted as F-8) is withdrawn.
  • F-3 / F-4: a boundary on its fallback; only a hold on the arming frame hides a fresh boundary's fallback.
  • F-6: a probe (isPending/latest) alone holds nothing; a plain read beside it is a read (Adding combined isPending breaks atomic publication #3442).
  • F-9 / D-23 / D-24: no committed value, no verdict — isPending of a first load or born-held memo throws when owned, false unowned; latest is its pending value if it has one.
  • D-4 / D-12: membership is decided by the render effect that reads a derivation, not where the memo was created; an unread memo holds nothing ([2.0] Mounting a memo during a held action can crash a render callback #3802).
  • D-16: a header that only calls isPending/latest does not hold a re-arm.
  • D-25: a node a hold's fetch is waiting on is that hold's work, not a verdict reader (isPending/latest on a superseded transition depend on whether the parked async memo also reads them #3884).
  • Statuses point at the implementing PR (carve branch) and its tests; OPEN-QUESTIONS.md keeps only the U section (U-1/U-2 built, U-3 noted).

Implemented by #3977.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LVm3wseHt9wRTY8LfPiHmQ


Generated by Claude Code

ryansolid and others added 3 commits October 9, 2026 22:20
- Holds: only a render effect whose read throws NotReadyError holds a
  change; a served stale value or a verdict read does not.
- Fresh and re-armed boundaries (#3540 stands): a new or re-armed Loading
  catches every NotReadyError under it, and its fallback lands with the
  frame that armed it. A separate change shows the fallback now; only a
  hold on that frame hides it. The draft that hid it for any outside
  reader of the same value is withdrawn.
- An uncaught first load or born-held read in a branch a change mounts
  holds that change; the branch never attaches with an empty slot.
  Loading is how a change escapes the hold. The direction rule's time
  half (once F-8) stays withdrawn.
- Verdicts: a node with a committed value is judged wherever its readers
  are. A node with none (a first load, a memo born held) is loading and
  has no verdict: isPending throws like its read when owned, false
  unowned; latest returns a born-held memo's pending value.
- Scenarios S-3, S-5, S-6, S-14, S-26 corrected; Q-1..Q-5 removed from
  OPEN-QUESTIONS now that the rules carry them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVm3wseHt9wRTY8LfPiHmQ
F-6: a probe alone holds nothing; a plain read beside it holds (#3442).
D-4: membership is the reading render effect's; an unread memo holds
nothing (#3802). D-16: the plain-read exception. D-2/D-12/D-13/D-23 and
U-1-U-3: status for carve/born-in-col2, carve/probe-not-holder and
carve/reader-position; the D-13 gap's outside-reader form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVm3wseHt9wRTY8LfPiHmQ
…25, #3884)

D-23 points to it, and notes the verdict-reader screen holds in either
read order (verdict-read-order.test.ts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVm3wseHt9wRTY8LfPiHmQ
@changeset-bot

changeset-bot Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7acdc0d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

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 +23 B 7.45 KB ✅
signals: + createStore 14.70 KB 0 B 0 B +13 B 14.71 KB ✅
signals: + isPending/latest 9.73 KB 0 B 0 B +21 B 9.73 KB ✅
app: render + one signal (the simple-app floor) 9.94 KB 0 B 0 B −48 B 9.94 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.70 KB 0 B 0 B −664 B 17.93 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.09 KB 0 B 0 B −686 B 29.27 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.94 KB 0 B 0 B −61 B 12.98 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.53 KB 0 B 0 B −44 B 14.56 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.82 KB 0 B 0 B +12 B 28.89 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.13 KB 0 B 0 B −65 B 10.13 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.44 KB 0 B 0 B −38 B 25.46 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.19 KB 0 B 0 B −657 B 31.39 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.43 KB 0 B 0 B 0 B 11.43 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.90 KB 0 B 0 B −2325 B 34.33 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.79 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.67 KB 0 B 0 B −2325 B 38.15 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.78 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) 34.73 KB 0 B 0 B −3490 B 35.66 KB ✅ assets.js 0.78 KB, bind.js 1.82 KB, decode.js 6.23 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.76 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.30 KB 0 B 0 B −3451 B 41.21 KB ✅ eager (counted): web.js 21.26 KB; assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.76 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.19 KB 0 B 0 B −2331 B 41.79 KB ✅ assets.js 0.78 KB, bind.js 1.84 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.59 KB, trace.js 8.80 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 46.99 KB 0 B 0 B −2288 B 47.49 KB ✅ eager (counted): client.js 27.48 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.81 KB, server.js 1.02 KB, serverForms.js 3.32 KB, trace.js 8.78 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).

@coveralls

coveralls commented Oct 11, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38121964477

Coverage remained the same at 77.429%

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: 1321
Covered Lines: 1079
Line Coverage: 81.68%
Relevant Branches: 1036
Covered Branches: 746
Branch Coverage: 72.01%
Branches in Coverage %: Yes
Coverage Strength: 26.99 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 2 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 195 untouched benchmarks


Comparing docs/async-fundamentals (8378501) with next (a497d4b)

Open in CodSpeed

ryansolid commented Oct 11, 2026 •

Copy link
Copy Markdown
Member Author

Review at 33d5ead. Good to merge: it merges cleanly with #3977, and rules-index.test.ts / spec-async-semantics.test.ts pass on the combined tree. Three non-blocking notes:

  1. It documents fix(signals): carve async toward the F/D rules; redo #3937, #3954, #3895 #3977's behaviour. The text cites carve/born-in-col2, carve/probe-not-holder and carve/reader-position, says "on next it still pins the old screen" (D-4) and "next still shows child 2 beside parent 1 1" (U-1), and points at tests/fresh-boundary-born-held-reader.test.ts, which only exists in fix(signals): carve async toward the F/D rules; redo #3937, #3954, #3895 #3977. Those lines go stale the moment fix(signals): carve async toward the F/D rules; redo #3937, #3954, #3895 #3977 lands; worth a follow-up pass to replace the branch names with the PR number.

  2. "isPending throws like its read" is not what fix(signals): carve async toward the F/D rules; redo #3937, #3954, #3895 #3977 ships. For a memo born held, read inside an owner, the engine returns false, joins the reader to the hold, and the pass continues past the probe (checked in a render effect and a memo; unowned is false, as documented). fix(signals): carve async toward the F/D rules; redo #3937, #3954, #3895 #3977's own description says "the owner joins the hold". D-23's guard example (if (!isPending(x)) save(x())) argues against exactly that false. Affected: F-9, the "First load" vocabulary entry, D-2, D-12, D-23, D-24, S-3, S-14. Either the wording becomes "joins the hold" or the engine throws.

  3. S-3's "Effects ran" column disagrees with its pin. tests/born-held.test.ts (unchanged by fix(signals): carve async toward the F/D rules; redo #3937, #3954, #3895 #3977) still has the direct <span>{x()}</span> effect run with 0 at the mount and again at the commit. The table matches a Show-driven mount, where nothing runs until the commit, not the direct mount the pin covers.

— Claude via Claude Code


Generated by Claude Code

- Branch names replaced with #3977; statuses no longer describe next.
- D-12 status notes the inside-boundary creation gap (it.fails pin).
- S-3: the pin is a direct mount; its direct binding shows the committed
  value and re-runs at the commit (D-3). The click-mounted table stands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVm3wseHt9wRTY8LfPiHmQ

Copy link
Copy Markdown
Member Author

Addressed in 813d6a21:

  1. Branch names replaced with fix(signals): carve async toward the F/D rules; redo #3937, #3954, #3895 #3977; statuses no longer describe next.

  2. S-3: the pin is a direct mount — a note under the table gives its screen and effect runs (the direct binding shows 0, then 1 at the commit, D-3); the click-mounted table stands, unpinned.

  3. ("throws" vs "joins the hold") is a ruling — raised with the maintainer; wording or engine follows that.

🤖 Generated with Claude Code


Generated by Claude Code

…review)

Ruling 2026-10-11: "throws" was to make the hold or the boundary
trigger, and born held already does. Inside an owner, isPending of a memo
born held holds its reader as its read does (caught by a boundary that has
not shown content, or held with the change) and the reader runs at the
commit; a first load's NotReadyError is unchanged. No answer shows before
the commit — the web guard test pins <input disabled={isPending(data)}>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVm3wseHt9wRTY8LfPiHmQ

Copy link
Copy Markdown
Member Author

Item 2, ruled: "throws" existed to make the hold or the boundary trigger, and born held already does. The engine stays as #3977 ships it; the wording now says what happens — inside an owner, isPending of a memo born held holds its reader as its read does (caught by a boundary that has not shown content, or held with the change) and the reader runs at the commit; a first load's NotReadyError is unchanged; unowned is false. Updated in F-9, D-2, D-12, D-23, D-24, S-3, S-14 (8378501d, plus undo an accidental reformat).

The guard D-23 cares about is pinned in #3977: packages/web/test/ispending-guard-born-held.spec.tsx — <input disabled={isPending(data)}> is never seen enabled before the commit (held branch, or the fallback under a fresh Loading).

🤖 Generated with Claude Code


Generated by Claude Code

@ryansolid
ryansolid merged commit c51c5c9 into next Oct 11, 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.

3 participants