signals: the hold model (L2) — core rebuilt from its rulings; −23% floor, −18% hello world, 0 regressions - #3774
Conversation
…/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)
…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 detectedLatest commit: c57018c The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
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 |
…-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>
Size (brotli, eager entry chunk)
Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. Caps in |
Coverage Report for CI Build 37187361968Coverage remained the same at 75.991%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
…5 B min), accepted by the maintainer Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Merging this PR will regress 14 benchmarks
|
…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>
…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>
Summary
The signals core rebuilt on the hold model (L2) — the size-reduction carve of 2026-09-30 → 2026-10-04, off
next@309b08730, withnext'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 inpackages/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)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)
create1to1+6.9%,update1to1+5.6%,propagation:diamond+66%,avoidable+4.5%.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).Size caps
Size-Exception:
frames: eager client consumer12.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 takesownedWrite; the bytes are twoboundlocals and the option. No floor cap infloor-caps.jsonis raised.Public API
No public API change from the carve.
#3753(fix!: lowercaseon*names are attributes, not event handlers) isnext's own breaking change, reapplied here by cherry-pick.Core seams added (internal)
handleAsync(el, result, setter, onError?)— 4th optional parameter;propagateStatusexported fromcore/async.ts;stagedScreenexported fromcore/core.ts;REACTIVE_PROBE_UNANSWERED;GlobalQueue._storePark,_storeCommit,_laneSeams,_laneRebase,_slotCovered,_slotFlight,_storeMarks,_witnessMarkand the other late-bound hooks the modules install (seeGlobalQueue's static declarations and INTERNALS §0).Reapplied from
next11 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 needede26bb58c3). #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
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.