Skip to content

fix(signals): serve an inherited posture the hold's write from node-less store keys - #4682

Merged
ryansolid merged 2 commits into
solidjs:nextfrom
devagrawal09:tembo/nodeless-store-inherited-posture
Nov 4, 2026
Merged

ryansolid merged 2 commits into
solidjs:nextfrom
devagrawal09:tembo/nodeless-store-inherited-posture

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

Builds on #4681 by @GabbeV. His commit (68be911d, unchanged, authored by him) fixes the TypeError that halted reactivity when an untracked staged-store read sat beside latest() on a store with no reactive node. This PR adds one commit on top. It extends that guard so node-less store keys also follow the #4544 ruling, instead of skipping it.

Problem with the guard alone. #4625 added the ruling in RULES-DERIVED D-25 ("an inherited posture is not a probe", #4544): a pass whose verdict posture is only inherited is served the hold's own sync write. INTERNALS-STORE-STATE R2 says stores follow signal rules by default. The guard skips holdStaging whenever stagedScreen gets no node. So on a store key with no node, an inherited-posture pass was served the committed value instead, while the same read on a signal, or on a key with a node, was served the staging. Whether the store had a node, an internal detail, changed what users saw:

  • Inherited isPending: at the landing, a render effect saw a torn frame (data landed, count still 0), then the corrected one in the same flush. The signal goes straight to the corrected frame.
  • Inherited latest: the pass derived 0/0 mid-hold instead of 0/1, and re-ran an extra time after the landing.

Fix. holdStaging accepts a missing node. With no node it applies only the reader-posture checks: no probe of its own (CONFIG_OWN_PROBE), not a frame, no open window or unanswered probe, and the lane condition. It skips the node-specific checks, because a node-less staging is by construction this flush's own plain sync store write. D-26 atomicity ([latest(x), x()] through a memo) is unaffected: a key that a latest() window asked about always gets a leaf node, so it reads through the node path, where _askedIn still decides. I traced this, and the D-26 case below pins it.

How did you test this change?

  • New S9 block in tests/posture-store-parity.test.ts (23 tests). Six sources: signal, untracked signal, tracked store, and untracked store with a container node, with a leaf node only, or with no node. Each runs under an inherited isPending, an inherited latest, and an own-latest posture, and must match an exact trace. Each test also asserts which nodes exist, so the labels can't drift. A D-26 case pins store/signal parity for latest of the same key through a memo.
  • Posture oracle: visibility-oracle-posture.test.ts gains a memoInheritedUntracked reader kind (discovery coverage). Its readers are built after the write rather than re-run in the write's flush, so it does not reproduce this bug: all 160 new cells match memoUntracked, and the report is identical before and after the fix. S9 is the pin.
  • Docs: D-25's 2.0.0-rc.20: a memo that calls isPending()/latest() on an unrelated signal makes a branch opened by a held update read the pre-hold world; <Loading> never settles #4544 paragraph records node-less store parity, with S9 as a pin. docs/RULES-INDEX.md is regenerated (rules-index.test.ts passes). The changeset is updated.
  • Signals: build:js and types pass. Full suite after the build: 479 files, 6,689 passed, 14 expected failures, 2 skipped. The focused files (S9, nodeless-staged-verdict, verdict-memo-held-branch-4544, latest-memo-inline-parity) also pass under SIGNALS_TIER=prod and observe (102 each).
  • Web, against the built signals:
    • Hydrate config (includes test/consistency, c11-trace-equals-oracle): 758 passed, no failures.
    • verdict-memo-held-branch-4544.spec.tsx: passes.
    • Client config: 1,699 passed. The 7 failures are in dist-nested-manifests: web type declarations were not built locally.
    • Server config: 2,006 passed and 1 failed, server-functions-adapter-request. It fails identically without this commit (local Node 22.20).
  • Size (Rolldown 1.2.11, signals scenarios vs fix(signals): guard node-less store staging reads #4681's head): +isPending/latest +4 B minified / +7 B brotli (12,061 B vs a 12,070 B cap). The other three scenarios are unchanged. All within caps; no caps changed.
  • Prettier, git diff --check, and node scripts/check-release-invariants.mjs pass.

@changeset-bot

changeset-bot Bot commented Nov 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8e6ab2

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

This PR includes changesets to release 15 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
board-example Patch
canvas-example Patch
server-dbmon-example 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

@ryansolid
ryansolid merged commit 5208944 into solidjs:next Nov 4, 2026
8 checks passed
@codspeed

codspeed Bot commented Nov 4, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 201 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing devagrawal09:tembo/nodeless-store-inherited-posture (c8e6ab2) with next (2105ffb)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

ryansolid added a commit that referenced this pull request Nov 5, 2026
Five rows, +111 B minified each from #4677 and +7 B from #4682 (not re-recorded yet): signals + isPending/latest, app hydrating + every store family, the live page (a frozen floor, Size-Exception in the PR body), the compiled live page and live + router. Measured locally over next @ 5208944 with the in-repo compiler build; caps are measured brotli + 10 B.

Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>
ryansolid added a commit that referenced this pull request Nov 5, 2026
The same five rows, now +527-529 B minified from #4677 (both cases) and +7 B from #4682: signals + isPending/latest, app hydrating + every store family, the live page (a frozen floor, Size-Exception in the PR body), the compiled live page and live + router. Measured locally over next @ 5208944 with the in-repo compiler build and re-verified after merging next @ f6b6737 (#4688 is size-neutral on every row); caps are measured brotli + 10 B.

Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>
ryansolid added a commit that referenced this pull request Nov 5, 2026
#4687)

* fix(signals): a lane reveals whole beside its parent's landing (#4677)

An async memo over isPending(x) showed 'pending: true' beside an isPending(x) binding still false: the parent landing at the lane's seam voided the binding's lane run while the memo's run showed. The voided runs now show with the lane's (F-6, F-1); the landing's re-derivation follows next round. The second order from the issue (a landing flush's own run shown beside held runs parked with a re-derivation, F-1/F-7) is pinned it.fails.

Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>

* chore(size): re-record lane rows after #4682 and #4677

Five rows, +111 B minified each from #4677 and +7 B from #4682 (not re-recorded yet): signals + isPending/latest, app hydrating + every store family, the live page (a frozen floor, Size-Exception in the PR body), the compiled live page and live + router. Measured locally over next @ 5208944 with the in-repo compiler build; caps are measured brotli + 10 B.

Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>

* fix(signals): hold a landing's plain runs for its re-derivation (#4677)

Case 2 of #4677: when the lane lands first, the write's landing re-derives the badge memo onto a new flight and holds its runs for it (U-5), but that flush's runs of effects deriving plainly from the landing still ran, so details showed Item 1 beside id 0 (F-1, F-7). The landing walks the effects that derive plainly from what it commits (stopping at verdict readers and lane runs, which show now: F-6, D-25, #4398) into a round-scoped set, and verdict.ts attachWaits re-queues their runs to the next round, with the landing's held runs. The set is dropped at the end of the round, so nothing outlives the flush.

Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>

* chore(size): re-record lane rows for #4677 case 2

The same five rows, now +527-529 B minified from #4677 (both cases) and +7 B from #4682: signals + isPending/latest, app hydrating + every store family, the live page (a frozen floor, Size-Exception in the PR body), the compiled live page and live + router. Measured locally over next @ 5208944 with the in-repo compiler build and re-verified after merging next @ f6b6737 (#4688 is size-neutral on every row); caps are measured brotli + 10 B.

Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>

---------

Co-authored-by: tembo[bot] <208362400+tembo-io[bot]@users.noreply.github.com>
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.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