Skip to content

signals: the hold model (L2) — core rebuilt from its rulings; −23% floor, −18% hello world, 0 regressions - #3774

Merged
ryansolid merged 48 commits into
nextfrom
size/carve-step1
Oct 4, 2026
Merged

ryansolid merged 48 commits into
nextfrom
size/carve-step1

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Summary

The signals core rebuilt on the hold model (L2) — the size-reduction carve of 2026-09-30 → 2026-10-04, off next @ 309b08730, with next's 11 subsequent commits reapplied on top. Nothing here is a port behind existing seams: the transition layer, the optimistic engine, the verdict machinery and the store were rebuilt from their rulings, each step measured on the size suite and the full test matrix, every re-pin listed by name.

The design record is documentation/plans/size-reduction-carve-step1.md (§1–§42). The rulings are in packages/signals/docs/SPEC-ASYNC-SEMANTICS.md ("The hold model — L2") with every A-rule's Mechanism paragraph rewritten against the running code; the pre-L2 index is under History verbatim.

Size (br, vs next @ fork)

scenario next this PR Δ
signals: core floor 9508 7315 −23.1%
app: render + one signal 11970 9801 −18.1%
app: CSR (Show/For/Loading/Errored/lazy) 14901 12807 −14.1%
app: hydrating (no stores) 19663 17632 −10.3%
signals: + createStore 16848 14435 −14.3%
signals: + isPending/latest 12160 9438 −22.4%
app: hydrating + every store family 30789 28721 −6.7%
page: base server components 46193 43936 −4.9%
page: live server components 50442 47588 −5.7%

Minified −7 to −11 KB on every app scenario. The store layer's own bytes are −5% (plan §35.1: the remaining bytes are feature surface, each pinned; the maintainer ruled none of the levers taken).

Tests

signals 4892 / 0 (256 files; 2 expected-fail, 2 skipped), web 1132 / 0, solid 819 / 0, compiler 5957, babel-plugin 271, h 70, html 206. Zero passed→not-passed at every step against the previous step; every re-pin is named in the plan with the rule it moved to (§21, §27, §40.2, §41–42) and the maintainer's ruling on it.

Benchmarks (fork vs this PR, interleaved, same sitting — plan §41)

  • Signals, built dist: create1to1 +6.9%, update1to1 +5.6%, propagation:diamond +66%, avoidable +4.5%.
  • Stores: deep wrap +32–42%; writes ±; keyed reconcile of 1k rows 100%-changed ahead of next; dbmon two-arm A/B at parity on every op except dbmon-deep tick 1.08–1.13× (harness method), accepted within the band; jfb (signal / shallow / deep) and the reorder matrix at parity (interleaved same-browser).
  • In-repo vitest benches disagree with the dist on the same shapes (plan §12.5/§41.1: vite-node pays a namespace load per cross-module reference; the carve moved rules into modules). CodSpeed will show those.

Size caps

Size-Exception: frames: eager client consumer 12.98 → 13.00 KB (+21 B, 12,997 B measured) — the frames client's address switch re-arms the shell gate and rebinds in the pass that sees the new address, not in an effect's run; under the hold model the run was stashed behind the very gate it would release, so a second switch mid-flight never bound to the live call (call-driven-lifecycle). The gate signal takes ownedWrite; the bytes are two bound locals and the option. No floor cap in floor-caps.json is raised.

Public API

No public API change from the carve. #3753 (fix!: lowercase on* names are attributes, not event handlers) is next's own breaking change, reapplied here by cherry-pick.

Core seams added (internal)

handleAsync(el, result, setter, onError?) — 4th optional parameter; propagateStatus exported from core/async.ts; stagedScreen exported from core/core.ts; REACTIVE_PROBE_UNANSWERED; GlobalQueue._storePark, _storeCommit, _laneSeams, _laneRebase, _slotCovered, _slotFlight, _storeMarks, _witnessMark and the other late-bound hooks the modules install (see GlobalQueue's static declarations and INTERNALS §0).

Reapplied from next

11 of the 12 commits since the fork, cherry-picked in order (-x): #3730, #3751, #3737, #3758, #3741, #3755, #3753, #3756, #3760, #3765, and #3742 (whose core already held on this branch — its pins pass; its docs/tests/changeset taken; one of its pins needed e26bb58c3). #3740's frame-scoped mask (reverted by #3742 upstream) is not picked.

Open for review

The re-pins the maintainer approved ("all four stand", 2026-10-03); the four store size levers deliberately not taken (§35.1); the deep tick's last ~10% (§41.5); the Q-D unkeyed positional residue (documented, §39).

Co-authored-by: Claude via Cursor

Size exceptions

  • frames: eager client consumer cap 12.98 → 13.00 KB: 12,997 B br / 41,048 B min against next @ 41fdf9696's 12,977 B / 40,973 B (+20 B br, +75 B min). An address switch now re-arms the shell gate and rebinds the frame in the compute pass that sees the new address, not in an effect's run. Under the hold model that run was stashed behind the gate it would release, so a second switch mid-flight never bound to the live call (call-driven-lifecycle). Accepted by the maintainer.

Size-Exception: frames eager client consumer 12.98 -> 13.00 KB (+20 B br / +75 B min) — the address switch's re-arm and rebind move into the compute pass under the hold model. Accepted by the maintainer.

ryansolid and others added 30 commits October 2, 2026 14:49
…/actions — state at §27.2

Checkpoint of the measurement branch before the §28 replay of the lane
layer. The reactive engine rebuilt on the L2 hold model (transactions as
the one hold relationship), with lanes, verdicts, boundaries, reveal and
actions as modules behind GlobalQueue hooks; stores, affects() and the
attribution engine carved out for later steps. Rulings and per-step
measurements in documentation/plans/size-reduction-carve-step1.md.

Signals 4759: 2125 passed, 2536 carved, 97 failed (66 pre-existing, 15
lane-family, the rest store/affects); web 888, solid 678. Floor 7202 br.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
…8 replay

Two imports stubbed (createOptimistic's write; isPending/latest), the two
files removed; the core's lane touch points stay as dead, gated code until
S0 rewrites them. Measured: + isPending/latest 9025 → 7250 br (−1775),
page live 39519 → 37710 (−1809), floor 7202 → 7193. Suite 1612 passed
(303 pins carved, 210 in the halt cascade behind them). Numbers and method
in the plan doc §28.15.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… verdict.ts)

Three places per node, each meaning one thing: _value the committed truth,
_pendingValue a transaction's staging, _x._lane the lane's value; the lane
carries shown/held. Seat from carriage; leaves read the screen; a fresh lane
is judged from its first round; body-end corrections at the seam with one
more pure round (#3409); a dissolving lane drops its runs and retires its
flights; lane work reads unparked staging through and a park repairs the
leak. Removes CONFIG_LANE_HELD, CONFIG_HELD_TRUTH, REACTIVE_VERDICT_RERUN
and the seam swap. Two core fixes surfaced: merge resolves both ends
(a resolveTx cycle), and updateIfNecessary keeps the pass-verdict flags on
a pull that recomputed nothing.

Signals 2128 passed (pre-replay 2125), 0 passed→not-passed vs the carved
and the pre-replay baselines; web 888, solid 678. Floor 7222 br (+20),
+ isPending/latest 9213 (+188) — the consolidation is S2 (verdict
unification). Design amendments and numbers in the plan doc §28.15.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…latest(dep), V5)

A leaf reading a lane flight that has never shown waits on it in its own
frame and reads the landed answer at the landing — no new edge, as 28.6
predicted. blocked() records the judged transaction: a zombie is moot for
the commit that disposes it alone; a landed lane's held runs release at
the parent's landing; a lane-dirty zombie runs now. A derivation committed
at a dissolve that re-derives publishes its inputs. flip() lists the
boundary's output in the arming lane at once. blockedBy counts every stale
reader of a flight, lane work included (V5, red since §20). Three re-pins
to the lane rulings (body-end visibility, #3479 boundary display-ahead,
carve-out gates).

Signals 2138 passed (S0 2128; the §28.12 target), 0 passed→not-passed vs
S0 and vs the carved core; web 888, solid 678. Floor 7212 br (−10 vs S0),
+ isPending/latest 9269 (+56). Details in the plan doc §28.15.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…Readers, staleReader, dissolveLane by outcome)

The verdict watchers merge into the lane layer's stagedReaders (a verdict
reader before the frame's verdict is the same reader as lane work that read
the frame's staging); the four stale-reader registrations become one
staleReader(c, t); observeFlight is verdictRead with a forced rerun; the
verdict HELD arm reads the lane's seam verdict (_held) instead of walking
blocked(); the OVERRIDE arm's tail is display(). dissolveLane restructured
by outcome, with every guess of a dissolving lane re-homed with its truth
(the body-end pre-staging loop was the general rule). verdict.js 2477 →
2091 min; lanes.js 4212 (the §28 rules). The rest of verdictValue stays:
its arms are value semantics, not membership.

Signals 2138 / web 888 / solid 678, 0 passed→not-passed vs S1 and the
carved core. Floor 7208 br (+6 vs pre-replay), + isPending/latest 9212
(+187 — the rulings' rules, each pinned; recorded in §28.15).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…); GlobalQueue hooks declare-only

A mark is a count on the node (_x._marks) listed with its scope (the
transaction's _marks, released at land; or the ambient list, released at
the seam); coverage of derivations is pull-derived at probe time (the walk
over the probed node's current deps, error outranks); verdict readers
re-derive at registration and at the last release. A marked probe in a
transaction's flush is its verdict lane's work (display-ahead, not parked).
Store targets return with the stores. The 23 hook statics on GlobalQueue
emitted as 'static X;' under esnext — now declare-only.

Signals 2156 passed (+18, every signal-half affects pin), 0 regressions vs
S2 and the carved core; web 888, solid 678. Floor 7189 br (−19 vs S2; below
pre-replay and carved), hello world 9688, CSR 12684. Plan doc §29.

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

The attribution engine stayed intact through the carve; its feed did not.
This re-feeds it from the L2 hold model:

- holds: holdStart at the parking seam, holdEnd after the next un-parked
  flush's effects, transitionSettled in land, transitionMerged in merge
- hold census: nodes = t._nodes, action = t._open > 0, blockers =
  blockersOf(t) (pending non-effect nodes blockedBy still finds; the
  reporter effect is not a blocker); acknowledgements from _laneGuesses(t)
  (optimistic), t._marks (affects), and CONFIG_VERDICT readers' observe-only
  _devWindows bits (isPending / latest)
- optimistic reverts: supersede / dissolveLane fire for a SHOWN guess
  replaced by a differing truth, "superseded" (a landing) vs "reverted"
  (correction found nothing beneath); the label is observe-only state, not
  a parameter (a parameter leaked ~46 min B into lanes.js)
- fallbacks: boundaryFallback on the swap to / from the fallback with the
  transaction the swap lands with; a mount's synchronous first show
  schedules the drain whose flushEnd is its display instant
- run posture: recomputeEnd's optimistic = lane work, transition = ran
  under a hold

Dev: the SETTLE_WALK_UNINITIALIZED_SOURCE tripwire excludes a lane
derivation's first landing (sits in the lane slot until the lane shows).

Re-pins (shape, not expectation): attribution optimistic-phase (guess in an
action's hold), settle-walk fake node (CONFIG_OVERRIDE + _x._lane),
untracked-read-after-await latest naming (no shadow node: names the source),
treeshake optimistic.ts -> lanes.ts, createMemo resolveAsync and
async-chain-supersession #3374 x3 (PRIMITIVE_IN_EFFECT_CALLBACK shapes:
resolve from the owning scope, compiled <Show keyed> remount),
scheduler-livelock (one heap).

Signals 2219 passed (+63), 0 passed->not-passed vs the affects commit and
the carved core; web 891 (+3: performance-tracks hold/fallback pins);
solid 678. Prod sizes unchanged except +isPending/latest +1 min B; observe
tiers carry the feed (CSR observe+attribution 28470 br vs next's 30654).
Plan doc section 30.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ontracts, phases, Q-list

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…s and order

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

A store is a tree of L2 nodes and nothing else carries reactive state:
one-literal slot nodes (slotSignal) for leaves, presence, the deep witness
and the container node — the $TRACK node given a value, whose _value is the
committed backing and whose _pendingValue is the pending backing, so the
scheduler owns the backing's lifetime (park/commit/revert on later steps)
and the store keeps no fold ledger. A write-created container nobody
subscribes to is released at the fold (transient staging home, INTERNALS §5).

Core seams (the two §31.7 named): CONFIG_SLOT_NODE dispatch at last-one-out
to the store's shared release hook (a leaf has no _x by design — jsfb's
sliding selection keys depend on the release); GlobalQueue._storeCommit
after commitPendingNodes. stagedRead/ownedScopeWriteMessage exported.
mapArray reads $TRACK again. _devWindows is now an observe-literal slot on
computed/effect (the attribution step had it as a write after construction;
found by the dist-artifacts literal pin, re-pinned).

Rules settled by pins: staging visibility is the pass's (`context`, not the
owner — onSettled/handlers see committed); adoption notifies at adoption
time against the view the nodes were last told (#3296; `ab` gone); the
enumerator check is per pass (_gen === _depGen).

Signals 2821 passed (+602), 0 passed->not-passed vs §30 and the carved core;
web 924 (+33); solid 691 (+13). Floor 7215 br (+26); `+ createStore` 11134
= floor +3919 br (gate <= +4000; next +7340). Remaining non-carved: holds
(S2), affects' store half (S5), rules-index, refresh-await.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…or untracked pass reads

Every read of a key with a leaf goes through core read(), tracked or not,
so the hold rules (frameRead / joinPass / A28) are core's and the store
restates none of them. Nodes are born from the two frames (bornStaged):
committed from the committed backing, a staging from the pending one when
they differ, held by the container's transaction when it is held — A29
born-held and #3706's per-key hold with no ledger. Structural reads take
the container node's frame (read(k): the node's value is the backing);
a lone descriptor read is gated by its presence node. Untracked reads of
un-noded keys by a pass ask the container without linking; a verdict
window with no reader judges the container only for a changed key (A22).
Adoption is staged (the container's _pendingValue), not eager; snapshot
sees the batch's own staging, not a held one. Fold hook after landings.

Core: read()'s held arm applies to untracked reads by a pass (fast block
and slow path) — pinned by posture-store-parity S4/S5's signal twins,
which had never run on L2. +2 min B.

Re-pins (flagged): posture-store-parity S6 flipped as its text instructs
(store follows signal); visibility-oracle-store structure rows' `latest`
violation re-pinned to the rule (structure rides nodes latest() tunnels).

Signals 2849 passed (+28), 0 passed->not-passed vs S1 and the carved core;
web 924; solid 691. Floor 7213 br; `+ createStore` 11269 (+135).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The firewall is projection-only and lives in the store module: every read
through a family target pulls the derive first — core read() of it,
untracked when settled (the barrier: no subscription), tracked while a
flight is up or errored (readers observe it as a memo's readers would;
the next settled pass pulls without linking and the stale link trims).
A render effect outside the flight's own flush is the frame, not a stale
reader of the derive. Pending propagates to leaf readers by notifyStatus's
own dependent rule (wakeFamily over the family's live index) and settles
at the landing from each leaf (settleFamily). A derive's continuation
writes join its hold (holdWithDerive). The creation run commits directly
(runDirect); later runs stage like a memo's recompute; a projection's
unheld staging is ahead for a handler. Adoption is eager (INTERNALS §3)
with the container node keeping the committed frame. Chained backings.

Core (flagged): recompute's self-registered-flight probe restored (a
projection's undefined return is not a sync answer — carved as store-only
in 1b, which it is); verdictValue's uninitialized-pending throw links an
untracked reader as read() does.

Re-pins (flagged): createStore.test #2692 trio -> A34 rule B store twins;
projection-slot-release counts the family's live index.

Signals 3185 passed (+336), 0 passed->not-passed vs S2 and the carved
core; web 931 (+7); solid 789 (+98). 19 projection reds open (S3b).
Floor 7257 br (+44); `+ createStore` 13302 (projections + reconcile
coupled; next 16848).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…store twins)

A pass reading a key the held batch left unchanged reads committed and
holds no one; a changed key reads the container's frame by core's rules
(read(k) untracked). keyChanged covers presence, enumerability,
accessor-ness; a swapped/non-plain prototype or a chained backing changes
every key. A mainline setter above a held adoption is a layer above the
held staging (the container node keeps the adoption; the draft is a
clone): keys the hold left unchanged read the ambient view. A user write
to a held derivation re-derives the fold under the hold (setMemo's twin);
CONFIG_MANUAL_WRITE marks a user proposal so a write made under the hold
keeps last-write-wins. holdWithDerive keyed on the setter's author.

Signals 3188 passed, 0 passed->not-passed vs S3a and the carved core.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…tore out of carved)

A user write to an optimistic store is a guess on the written key's node,
with presence and container-arrangement guesses (LaneView, judged by row
identity); landings confirm or correct beneath it, settle dissolves it; the
committed backing is never touched. Reads compose per key through the
nodes; a family's guesses stand in for its in-flight derive (A17); chained
views keep their links on the inner's truth (#3672); a user's reconcile
writes the draft. Late-bound (OptHooks): plain stores shed lanes.ts.

Core seams: inFlight/_slotFlight; untracked reads inside lane work derive
from the lane; a joined pass entering a lane is a staged reader;
linkBlocked mirrors blocked(k); supersede resolves the merged parent;
guessedValueOf/pendingGuessOf.

Signals 4707 passed (+1513), 0 passed→not-passed vs S3b and the carved
core; web 998 (+67), solid 812 (+23). 15 no-parent pins re-pinned to lane
contract 2 (three action twins added); S8/oracle supersede → the lane
rules; matrix F7 unpinned. Open: the overlapping-actions arrangement rows
(Q-D's replay evidence), see §35.

Co-authored-by: Cursor <cursoragent@cursor.com>
…), finding

writeOverride as a live export binding (the get-trap hot path no longer
calls an accessor); the optimistic draft + set-aside staging move into
optimistic.ts (OptHooks.draft; writes returns the staging); optRead()
for the five lane-view sites. + createStore 13968 → 13939, every store
family 27800 → 27783. 0 regressions (signals 4707, web 998).

Per-function attribution of store.js recorded in §35.1: the remaining
store bytes are feature surface (overlay draft, accessor keys, per-key
holds, projection+reconcile by #2883), not seams — the every-store gate
needs a ruling, not a trim.

Co-authored-by: Cursor <cursoragent@cursor.com>
… is closed

next's utils.ts verbatim with three import edits (symbols from
store/types, SUPPORTS_PROXY from constants, dead pendingCheckActive
import dropped). carved.ts has no stubs left.

Signals 4783 (+76, 0 regressions); web 1109 (+111, 0 carved); solid 813.
Page base/live 43402/46969 (next 46193/50442).

Co-authored-by: Cursor <cursoragent@cursor.com>
…tions green

store/affects.ts (installed with the stores): affects(store) marks the
record's $AFFECTS carrier and every live node under it, affects(record,
key) the slot's leaf; coverage by raw identity (#2882/#2904) — nodes born
in the window inherit (noteNode seam), untracked probes are witnessed
(verdict.ts _witnessMark, before the family pull — #2910), chained
backings followed to the base.

Signal half (flagged): ambient marks whose flush parks join its
transaction; a mark over a node whose own flight is up (or a slot node
whose family derive is — _slotFlight, now store.ts) stays to the landing
('nothing async below ⇒ no window', and its converse). settle passes the
parked transaction to _releaseAmbientMarks.

StoreNode type exported again as the L2 target (public API; flagged).

Signals 4808 (+25, 0 regressions); web 1109; solid 819/819 (declarations
regenerated). Floor +15 br, + createStore +49, every store family −38.

Co-authored-by: Cursor <cursoragent@cursor.com>
… the first landing (evidence + options, no pick)

Co-authored-by: Cursor <cursoragent@cursor.com>
…es (option 2′)

A landing is judged against the guess of the question it answers (#3331's
rule for derivations, now for store landings): the derive's writes carry
the flight's question; laneWrite holds an older question's truth beneath
the guess instead of correcting it; an arrangement guess on a keyed list
is re-based over the held truth by key (LaneView.removed keeps deliberate
deletes removed). Value optimism, uniform; replay deferred (§38/§39).

Core (flagged): a corrected SHOWN lane keeps this round's runs (the
body-end seam judged before the lane seam released them — web For over a
chained view lost a landing's row); a chained link's covered value is the
inner's live commit (_slotCovered). Verdict gate built then reverted —
#3331 pins isPending true over a differing stale truth (A24).

Store: an optimistic setter's first read births the draft over a staging
too (#2951: C's +1 composed on the truth, not the display); the get hot
path precedes the backing read and serves a chained guess node (A17).

New pin: tests/store/kanban-a17-fixture.test.ts (Gabriel's kanban).
Matrix 904/904, #2951 compose half, web F1 ×5, 3672 re-run pin.
Signals 4815 (0 regressions vs S5), web 1114, solid 819. Every store
family +670 br (the keyed re-base).

Co-authored-by: Cursor <cursoragent@cursor.com>
…an shape; one stale() for both landing paths

laneOutcome and laneWrite share stale(el, q): answered(el) (the newest
stamped source changed this round) for a derivation's pass, question for
a write. Behaviour unchanged. New pins run each shape on createOptimistic
nodes and on an optimistic store and assert the same frames.

Signals 4819 (+4), 0 regressions.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ia core propagation, promise-delivery readers by posture

Signals 22 → 3 reds (rules-index, PR-time); web 2 → 1; solid 0; 0 regressions vs qd8.

Engine: an unanswered probe (answered before the flush had a transaction) re-runs at the seam even when routed into the verdict lane (REACTIVE_PROBE_UNANSWERED); a render effect already observing a derive's flight keeps pulling it (linkedTo) — the frame's hold no longer drops with its flight up; the post-pass family wake requires a flight this pass registered (#3181); handleAsync onError seam + errorFamily (derive rejection reaches leaf readers); wakeFamily stamps leaves as pending-source carriers and propagates via core's propagateStatus (split from notifyStatus); refresh() does not launder a new question; quiet landings stamp their leaves' _reask; resolve/until/refresh waiter deliver own-transaction frames and defer over foreign holds (#3482/#3490); descriptor trap reads the frame's backing (#3706).

Re-pins (plan §40.2, maintainer to veto): A22-3, #3662 step 1, #3585 never-resolving case, #3038 companion walk.

Size: floor 7309 br (+46), +createStore 14169 (+94), every-store 28415 (±0), page live 47348 (+72).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ld by its own gate under L2)

Co-authored-by: Cursor <cursoragent@cursor.com>
…s skip descriptor probes under a staging

Found by the fork-vs-carve benchmark (plan §41). settleFamily ran on every derive commit, flight or not — O(all leaf nodes) per sync re-derive (a 1k-row derived store's no-op tick: 130 µs vs the fork's 9 µs); gated on fam.woke. keyChanged ran __lookupGetter__/__lookupSetter__/propertyIsEnumerable twice per key for every state[i] value read in the flush that staged a reconcile; a value read needs value + presence only (readSource's shape flag — the descriptor trap, bornStaged and the hold check keep the full test; a container with accessors seen keeps it everywhere). 10%-changed keyed reconcile of 1k rows: 996 → 400 µs (fork 270).

Signals 4838/3, web 1115/1, 0 pins moved. Size: +createStore +79 br.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… O(rows) slot re-resolve at the fold

From the jfb-deep two-arm A/B (plan §41.2/§41.4). readSource serves an unheld container staging directly to a plain pass already marked REACTIVE_STAGED_READ (not a verdict reader, not children-forbidden): an unchanged key is the same value in both frames, a changed one is what the mark already stands for — after a shift every index key had 'changed' and each read paid a closure + read() to set a mark the first had set. parentSlotKey short-circuits when the parent slot already holds this target's backing (a parent adopted whole carries its children's new raws before they fold; the indexOf(oldRaw) was a guaranteed O(rows) miss per changed row).

jfb-deep store ops at fork parity in node and interleaved in-browser; 100%-changed keyed reconcile 0.99 → 0.81 ms (fork 0.94). Signals 4838/3, web 1115/1, 0 pins moved; +createStore +16 br.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…re (14/14 pins hold)

The two suites imported the removed src/store/next/*; createStore / storeLookup / isOwned / $OWNER from the carve's store answer the same pins.

Co-authored-by: Cursor <cursoragent@cursor.com>
…old is live or a subscriber exists; allocation-free fold queue

stageOn: a staging gets its container node only when something can read the frame through it (a structural subscriber, lane work, a creation-time pass, a held derive); otherwise the backing swap and the fold queue are the whole staging. GlobalQueue._storePark materializes and holds the node for every node-less staging when the flush parks (pinned: a never-read key of a held container reads committed until the landing). getContainerNode born under a staging takes it as its own; committed()/readSource/bornStaged handle the node-less case. The fold queue is a reusable list (membership = pb !== null) plus a WeakMap of pre-batch backings written in place — the per-batch Map cost ~540 KB/tick of table allocation on dbmon's ~7000 containers and most of its GC.

dbmon-deep tick: node parity; browser 1.17× → 1.08–1.135× (harness method); remaining allocation is the per-adopted-raw lookup registration (§41.5). Signals 4853/3 (+1 pin), web 1115/1, solid 819/0; +createStore +124 br.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… the effect run (mid-flight second switch deadlocked under the hold model)

The follow effect's run was stashed with the frame the previous gate held, behind the gate the rebind would release (plan §40.3 diagnosis). Web 1116/0.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…3729) (#3730)

<textarea /> put the tokenizer in raw text mode because only the token
before the last was checked for a slash, so the rest of the template was
read as text. Check the last token as well; closing tags still have
their slash second to last.

Fixes #3729

(cherry picked from commit 6be8486)
…namic prop (#3728) (#3751)

The getter-wrapping exemption for `on*` names applied to components as well
as native elements, so `<${Loading} on=${() => key()}>` passed a function
that never changed. Components now exempt only camelCase `onXxx` handlers
(and `ref`); native elements are unchanged.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit a5d0e76)
…ted key onto the previous entry (#3737)

* fix(compilers,web): compiled SSR style objects no longer glue a computed key onto the previous entry

The compilers chained `ssrStyleProperty` calls with the `;` baked into each
later literal name; a computed key's name got none, so
`style={{ color: "red", [k]: v }}` rendered `color:redtop:1px`.

* test(web): run ssrStyleProperties spec under the server vitest config

The server config only includes test/server/**/*.spec.tsx, so the .spec.ts
file never ran in CI.

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

---------

Co-authored-by: Ryan Carniato <ryansolid@gmail.com>
Co-authored-by: Claude via Cursor <noreply@cursor.com>
(cherry picked from commit 98d35b9)
ryansolid and others added 11 commits October 3, 2026 23:48
…oid) (#3758)

The @solidjs/compiler-wasm32-wasi loader is generated by `napi build`, and
@napi-rs/cli 3.8.6 emitted a WASI preopen of the host filesystem root. On
Android/Termux `/` cannot be opened, so `new WASI(...)` failed with
UVWASI_EACCES before the compiler ran. napi-rs fixed the template upstream
(napi-rs/napi-rs#3485, in 3.9.1+): on Android the guest root maps to the
working directory, and the main thread passes that root to its workers.
Bump @napi-rs/cli to 3.10.6 so the published loader carries it.

The WASI test run gains a regression test that loads the binding with
`node:wasi` rejecting host-root preopens, as Termux does: it fails with the
3.8.6 loader and passes with the regenerated one.

Fixes #3748

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 41be120)
…ent (#3741) (#3757)

A dynamic() / <Dynamic> element whose prop getter mints a hydration id (a
ternary over a signal compiles to a condition memo inside the getter) and
whose children include an element hydrated with a key miss: the client's
runtime spread() inserted `children` before its attribute effect read the
getters, while the server's ssrElement read attributes first. Both draw on
the enclosing component's id counter, so the memo and the child element
swapped ids and the server's child was never claimed.

spread() now creates its attribute effect before the children insert -- the
order a compiled element already uses (its spread/attribute effects precede
its children inserts). ssrElement defers reading a `children` prop until
after every attribute (and the trailing compiled attribute thunk), so a
`children` key listed before an id-minting attribute agrees too.

Side effect on the client: a ref on a dynamic() element now runs before its
children are inserted, as on a compiled element.

Size: page: base +59 B brotli (+1 B minified), layout only; frozen cap
raised 46.20 -> 46.24 KB with a Size-Exception note.

Fixes #3741

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 365f37d)
…n a sheet loads early (#3755)

* fix(web): stylesheet-gated fragments reveal under a nonce CSP and when a sheet loads early

A fragment whose boundary streamed stylesheet links waited on inline
`onload`/`onerror` handlers. Those can't carry a nonce, so under a strict
`script-src` CSP the gate never released and the boundary stayed on its
fallback. The links now carry `data-dfc`, and the fragment helpers install
one capture-phase `load`/`error` listener, from the nonce-carrying task
script, that counts each link down once.

The links were also written ahead of the content template. A sheet that
loads before the parser reaches the template (a cached sheet, or a chunk
boundary between the two) ran `$dfc` while the template was missing, and
`$dfr` treats a missing template as a swap that already ran, so the reveal
was dropped. The template now goes first.

Fixes #3747

* test(web): remove the stylesheet gate spec's document listeners between tests

Each test evaluates the stream helpers, which install their capture-phase
listener on the shared jsdom document. Record and remove them so a test only
sees listeners its own scripts added, which is what the count-once test means
to exercise.

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

---------

Co-authored-by: Ryan Carniato <ryansolid@gmail.com>
Co-authored-by: Claude via Cursor <noreply@cursor.com>
(cherry picked from commit b57eb2e)
* fix!: lowercase on* names are attributes, not event handlers

Only `on` followed by an uppercase letter (`/^on[A-Z]/`) is an event
handler. Lowercase names such as `onclick` are plain attributes everywhere:
both compilers (DOM, hydratable, dynamic and SSR), the web runtime's
spread/assign and server spread walk, useHead, and the html/h element
paths. A leftover `on:click` lowering (to a `:click` listener) is gone;
the name is now an ordinary namespaced attribute.

Dev builds warn once per name with LOWERCASE_EVENT_ATTRIBUTE when a
function reaches setAttribute under an `on*` name. ssrAttribute now
escapes function values instead of interpolating their source raw.

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

* fix(html)!: component props follow h's getter rule, no onXxx exemption

On components every zero-argument function prop, `onXxx` handlers and
`ref` included, becomes a getter, matching @solidjs/h and 1.x
hyper-dom-expressions: a component handler declares its event argument.
This reverts #3751's unreleased component exemption. Elements keep the
`ref` / `onXxx` exemption; lowercase `on*` stays an attribute.

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

* perf(web): keep the per-key event check off the regex engine

The server spread walk and the client's assignProp test every key of a
spread for `onXxx`. A `/^on[A-Z]/` test there regressed the
spread-static-tail renderToString benches by 5-7% against the
`startsWith("on")` it replaced. Gate on `startsWith("on")` and compare one
char code, so a key not starting with `on` costs what it did before.
`ssrAttribute` takes strings first, ahead of the new function branch, and
h tests a prop's type before its name. Behavior is unchanged.

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

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 4f67697)
…rect the render root form (#3756)

* fix(h): materialize elements without mutating the caller's props

An h() element is a thunk that materializes each time its consumer reads
it, but materializing wrote to the caller's props object (children assigned,
zero-arity props turned into getters, class rewritten), so a second
materialization threw — e.g. a Show re-showing a child whose children are
h() elements. Props are now built copy-on-write.

A thunk returned from an accessor child reached insert's inner pass and was
re-created whenever its output changed (a Loading over async children never
settled). It is now materialized in the insert's tracking pass, where JSX
would create the component.

The README's render(() => h(App), el) / render(App, el) root forms hit the
same re-creation through render's insert; it now prescribes render(h(App), el),
and h's call type no longer includes the array passthrough so that form
type-checks.

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

* docs(h): lead with the render(h(App), el) rule

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

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 9e85a09)
…#3760)

* fix(web): a late hydration claim leaves server nodes in place (#3749)

A settled <Loading> whose lazy() module is still loading when hydration
starts returns nothing in the root pass and claims its server nodes on a
later resume. The insert holding the boundary's value sits outside the
boundary: its claim pass saw `undefined` and tracked the region as empty,
so on resume it took the client path (appendNodes / appendChild) and
re-inserted the connected, already-claimed nodes, moving them and
blurring a focused input.

insertExpression now leaves nodes in place when an untracked region
receives nodes already sitting in it in order. The check lives on the
hydration runtime slot, so client-only bundles carry none of it.

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

* fix(web): shrink the late-claim in-place check (#3749)

One guard at the top of insertExpression replaces the hydration-runtime
helper and its two call sites: an untracked region whose incoming nodes
are all already children of the parent returns without touching the DOM.
The first node rejects a fresh render without allocating. +90 B minified
on hydrating bundles instead of +180, and under every existing cap.

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

* fix(web): scope the late-claim in-place guard to hydration (#3749)

The guard now runs only while `sharedConfig.hydrating` is set: the root
pass and each boundary's resume window, where the #3749 landing happens.
Then claimed server nodes are the only nodes of ours already in the
parent, so order needs no check. After hydration completes, inserts
behave exactly as on `next`; a new test pins that.

Encoded without the first-node fast path (the allocation is now
hydration-only): +91 B minified on hydrating bundles, under every cap.

Also marks the #3741 page-base ledger note as accepted.

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

* chore(size): raise the page-base cap to 46.25 KB for #3749

The hydration in-place guard measures 46,249 B on the base
server-components page against `next` @ 9e85a09's 46,240 (+9 B brotli;
+92 B minified). Ledger note in scenarios.js.

Size-Exception: page base 46.24 -> 46.25 KB: #3749 hydration in-place guard (+9 B brotli, +92 B minified); accepted by the maintainer.

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

* ci: re-run the size gate against the PR body's Size-Exception line

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

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit e44b2e4)
A fork PR's token is read-only, so posting the report comment answered 403
and the compare job showed as failed on every external PR. The report now
also goes to the job summary, and the comment only runs on PRs from this
repository.

(cherry picked from commit 4738c6d)
)

A manual write to createSignal(fn) / createStore(fn) lands at once and never
re-runs the derivation on its own; a source change, in the same flush or
later, inside or outside an action, across a hold, re-runs it with the write
as `prev` (the store's draft). Reverses #2692's beta.11 same-tick precedence
(maintainer-approved): within a flush call order isn't observable, and for
stores a partial write dropped the whole fold. Supersedes #3740's
frame-scoped mask: the heap refusal, `_manualWriteTime` and the setter's
deleteFromHeap / DIRTY-CHECK clear are removed; REACTIVE_MANUAL_WRITE keeps
only its A34 proposal role. Refs #3733.

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

(cherry picked from commit 9a213bbb514ca313bdefc5bfe31d072e94409c65; the carve's core already implements rule B — its pins (createMemo R31 ×7, 3612 ×19, 3733 ×n) pass unchanged — so core.ts/heap.ts/types.ts keep the carve's text; next's docs, tests, changeset, signals.ts doc comment and solid hydration change are taken; src/store/next/* stays deleted)
Co-authored-by: Cursor <cursoragent@cursor.com>
… holder (#3733 action-hold pin)

readSource's draft/write-override arm: for the derive (write override, not a user write), a held leaf — or a held container whose held staging changed the key — is the future; the pass joins it (joinPassTx), so the re-derived result reveals with the action. Brought over with #3742's tests: 4890/3, 0 moved.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…MANTICS, amendments on A18/A19/A22/A29/A34, INTERNALS §0, RULES-INDEX regenerated

rules-index ×3 green (57 src ids resolving; every live A-rule cited by a test). src/ comments cite plan sections as 'plan sec. N' (the §N grammar is INTERNALS-STORE-STATE's). Plan §42 records the finish-out: signals 4894/0, web 1132/0, solid 819/0, compiler 5957, babel-plugin 271, h 70, html 206.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…he pre-L2 index moved to History verbatim

32 rules now carry 'Mechanism (L2, 2026-10-04)' against the running code (scheduler/core/lanes/verdict/boundaries/store by function); the 2026-09-14/15 paragraphs are under History as 'Pre-L2 mechanism index'. RULES-INDEX regenerated; rules-index 3/3. Plan §42 records the 2026-10-04 rulings (store gates: none; deep tick: accept; Q-D unkeyed: leave; fate: one PR against next).

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

changeset-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c57018c

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
@solidjs/web Patch
test-integration Patch
@solidjs/babel-plugin Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
todos-server-example Patch
@solidjs/compiler Patch
solid-js Patch
@solidjs/universal 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 and others added 4 commits October 4, 2026 00:21
…-step1

The branch already carried next's 11 earlier commits as cherry-picks; this brings the one since (#3772). signals/* unchanged by the merge; src/store/next/* stays deleted.

Co-authored-by: Cursor <cursoragent@cursor.com>
…tion census moved with #3742's treeshake note)

Co-authored-by: Cursor <cursoragent@cursor.com>
…arve-step1

Co-authored-by: Cursor <cursoragent@cursor.com>
…+21 B)

Size-Exception: the frames client's address switch re-arms the gate and rebinds in the pass, not the effect run (#3774, e133516) — under the hold model the run was stashed behind the gate it would release; the two `bound` locals and the `ownedWrite` option are the bytes. Measured 12,997 B against next @ 41fdf96's 12,976. Pending maintainer review.
Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.32 KB −2177 B (−22.9%) 7.33 KB ✅
signals: + createStore 14.52 KB −2326 B (−13.8%) 14.53 KB ✅
signals: + isPending/latest 9.44 KB −2710 B (−22.3%) 9.45 KB ✅
app: render + one signal (the simple-app floor) 9.80 KB −2175 B (−18.2%) 9.81 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.63 KB −2054 B (−10.4%) 17.64 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 28.77 KB −2045 B (−6.6%) 28.78 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.81 KB −2072 B (−13.9%) 12.82 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.38 KB −2040 B (−12.4%) 14.39 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.60 KB −2011 B (−6.6%) 28.61 KB ✅ lazy-page.js 0.04 KB
frames: eager client consumer (frames client + transport, lazy codec) 13.00 KB +20 B (+0.2%) 13.00 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 44.02 KB −2232 B (−4.8%) 44.03 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
page: live server components (base + live/GET + action + isPending/latest) 47.66 KB −2772 B (−5.5%) 47.67 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.41 KB 0 B 20.42 KB ✅

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. Caps 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).

@coveralls

coveralls commented Oct 4, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37187361968

Coverage remained the same at 75.991%

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: 1195
Covered Lines: 962
Line Coverage: 80.5%
Relevant Branches: 925
Covered Branches: 649
Branch Coverage: 70.16%
Branches in Coverage %: Yes
Coverage Strength: 27.92 hits per line

💛 - Coveralls

…5 B min), accepted by the maintainer

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

codspeed Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 14 benchmarks

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

⚡ 27 improved benchmarks
❌ 14 regressed benchmarks
✅ 144 untouched benchmarks
⏩ 3 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ store reconcile tree reverse: 1111 keyed nodes 26.6 ms 30.8 ms -13.39%
❌ updateSignals:update1to1 69.4 ms 78.4 ms -11.49%
❌ createComputations:create0to1 53.7 ms 60.4 ms -11.07%
❌ dbmon shallow full tick 87.5 ms 96.9 ms -9.63%
❌ createRenderEffects:create1to1 140.4 ms 154.8 ms -9.25%
❌ store reconcile tree shuffle: 1111 keyed nodes 24.7 ms 27.2 ms -9.02%
❌ reconcile: deep tree, all ~12k paths subscribed 72 ms 78.4 ms -8.07%
❌ reconcile: deep tree, single deep() effect 115.2 ms 124.7 ms -7.62%
❌ enumerate:for...in 1000 rows x 30 fields 154.7 ms 167.4 ms -7.58%
❌ createDispose:memoTree 74.8 ms 80.7 ms -7.27%
❌ updateSignals:update1to1000 2.8 ms 3 ms -6.97%
❌ reconcile: deep tree, 10 of ~12k paths subscribed 3.2 ms 3.4 ms -5.98%
❌ enumerate:Object.keys 1000 rows x 30 fields 152.7 ms 161.9 ms -5.72%
❌ dbmon full tick — deep reconcile 699.3 ms 740.4 ms -5.55%
⚡ spread store record × 500 34.6 ms 29.7 ms +16.38%
⚡ steady state: owned backings (#3360) 7.9 ms 6.9 ms +14.78%
⚡ readAllowed 32.4 µs 28.6 µs +13.2%
⚡ spread plain object × 500 9.8 ms 8.9 ms +10.09%
⚡ style store object, mutated in place × 500 40 ms 36.7 ms +8.85%
⚡ hasAllowed 26.2 µs 24.1 µs +8.85%
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

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


Comparing size/carve-step1 (22c3d3e) with next (41fdf96)

Open in CodSpeed

Footnotes

  1. 3 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 and others added 2 commits October 4, 2026 00:52
…one-key stores 2.1 → 1.27 ms per 2000 (next 1.13)

CodSpeed flagged 'fresh stores: create + first write + first commit' −13%; on the built dist it was 2×: every fresh draft recorded a weak-map entry for a backing that never moves until the fold. Adoptions keep the weak map (written in place for reused containers — dbmon's steady state is unchanged); drafts record nothing; a mid-batch privatization or a draft over an adopted raw is listed in a small per-batch map (preBatch). Signals suite green, web green.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
floor 9.51 -> 7.33 KB, render+signal 12.05 -> 9.81, hydrating 19.69 -> 17.64, page base 46.25 -> 44.03, page live 50.45 -> 47.67 (floor-caps.json); +createStore 16.85 -> 14.53, +isPending 12.16 -> 9.45, every-store 30.82 -> 28.78, CSR 14.94 -> 12.82, observe 16.48 -> 14.39, attribution 30.71 -> 28.61 (inline). Measured by CI at 22c3d3e; frames (13.00) and the server caps unchanged. Lowering is always allowed; raises need a Size-Exception.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 1b9ceb6 into next Oct 4, 2026
6 of 7 checks passed
ryansolid added a commit that referenced this pull request Oct 4, 2026
…atch record (CodSpeed follow-up to #3774) (#3776)

* bench(signals): benchmarks measure the built package

In benchmark mode the benches' src/index.js import is aliased to the dist
entry of the selected tier (SIGNALS_TIER, default prod for benchmarks) and
the dist is externalized so Node links it natively — the prod build
preserves modules, and vite-node's SSR transform would turn its
cross-module imports back into the namespace-object property loads the
alias exists to take out of the measurement. SIGNALS_BENCH=source keeps a
from-source loop for local iteration. A missing or stale dist fails the
run with the rebuild instruction instead of timing code not under test.

CodSpeed already builds before benching; its first run after this
re-baselines every signals bench. Plan sec. 43.1.

Co-authored-by: Cursor <cursoragent@cursor.com>

* store: the fold queue records the pre-batch backing beside the target

The weak map keyed by target kept every container's last adopted-away
backing alive until its next adoption: for a keyed reconcile the previous
tick's whole tree promoted out of the nursery every tick (saturated
listened-paths +15%, reconcile tree +10-13% over next on the dist, all of
it GC); for a store adopted once, the old tree for the store's lifetime.

The record is now foldOlds, parallel to foldList: t.v at queueFold for
every queued target, released with the drain. That one record is the
fold's base in every case (draft, eager adoption, mid-batch privatization,
draft over an adopted raw), so privatizedOlds, draftedAdoptions and the
adoption flag go. The target has no slot for it (ARRAY SHAPE RULE), so the
by-target lookups (preBatch: a node born onto an open staging, a
committed-frame read of a node-less staging, a park) index the list
through a map built on first use per batch; the drain never builds it.

After, on the dist vs next: tree reverse +3%, shuffle +4%, saturated
+1.5%, sparse parity; fresh stores / owned backings / enumerate / storebench
unchanged; GC totals identical to the head. Signals 4894/0, web 1132/0,
solid 819/0; every size cap holds. Plan sec. 43.2.

Co-authored-by: Cursor <cursoragent@cursor.com>

* bench(signals): projection-root-write loads under the dev tier

The warm-up store write sat inside createRoot, where the dev tier's
owned-scope write check throws — the file failed to load under
SIGNALS_TIER=dev, so CodSpeed compared its prod-dist values against a
stale base (two of the four 'regressions' on #3776). Moved outside the
root. Plan sec. 43.1 records the first dist-mode CodSpeed run.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

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.

5 participants