Skip to content

Store rewrite: single-home storage model replaces the legacy store - #3019

Merged
ryansolid merged 34 commits into
nextfrom
store-rewrite
Aug 19, 2026
Merged

Store rewrite: single-home storage model replaces the legacy store#3019
ryansolid merged 34 commits into
nextfrom
store-rewrite

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

Ground-up rewrite of the store subsystem on a single-home storage model: raw objects are committed truth (CoW-owned, privatize-on-first-write), per-property nodes are real core signals created lazily on subscription, writes park in copy-on-write pending backings that fold at flush, and transactions/optimism ride core lanes — the store-side transactional subsystem (override layers, layer clears, duplicate entanglement) is deleted wholesale. Design + decision log in packages/solid-signals/INTERNALS-STORE-STATE.md; semantics were mined from ~17k lines of existing store tests into 217 pinned rules before implementation (rules-mining/).

The legacy implementation is fully deleted (−3k lines): every public form — plain, shallow, derived, projections, optimistic — serves from the rewrite, with store.ts reduced to shared machinery and no transitional dispatchers.

Performance (vs shipped next, prod builds)

  • uibench creation ~25% faster (accessor scan deleted; hidden-class-stable targets)
  • dbmon ticks at statistical parity (interleaved thermal-fair A/B, n=100 ×3; block-sweep median next-faster 12.3 vs 12.8ms), sort consistently faster (~70/30 rounds), mount/remount/unmount parity
  • optimistic write-storm 0.94x (full write→display→settle→revert cycle)
  • Perf-critical findings recorded in the internals doc, including the per-node wrap cache (raw-as-truth's read-path tax vs legacy's pre-wrapped nodes) and the interleaved benchmark methodology that exposed what block-ordered runs masked

Size

  • Whole package −522 gzip vs next (bundlephobia-style, 22,845 vs 23,367)
  • Optimistic channel is now tree-shakeable: plain-store bundles drop ~0.5kb gzip (injection table installed by createOptimisticStore; all call sites fam?.opt-gated)

Fixes shipped bugs

Test plan

Made with Cursor

@changeset-bot

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 07d06df

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

This PR includes changesets to release 9 packages
Name Type
@solidjs/signals Patch
test-integration Patch
solid-js Patch
babel-preset-solid Patch
@solidjs/web Patch
@solidjs/html Patch
@solidjs/h Patch
@solidjs/universal Patch
@solidjs/element Patch

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.18 KB (+0.43% 🔺)
signals: + createStore 11.77 KB (-8.95% 🔽)
signals: + isPending/latest 8.84 KB (+0.44% 🔺)
app: render + one signal (the simple-app floor) 9.7 KB (+0.14% 🔺)
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 16 KB (-0.33% 🔽)
app: hydrating + every store primitive family 22.43 KB (-1.48% 🔽)
app: CSR with Show/For/Loading/Errored/lazy 11.78 KB (+0.13% 🔺)
frames: eager client consumer (frames client + transport, lazy codec) 10.14 KB (0%)

@coveralls

coveralls commented Aug 19, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 32223705775

Warning

No base build found for commit 2a49adc on next.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 72.158%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 917
Covered Lines: 711
Line Coverage: 77.54%
Relevant Branches: 728
Covered Branches: 476
Branch Coverage: 65.38%
Branches in Coverage %: Yes
Coverage Strength: 15.01 hits per line

💛 - Coveralls

@codspeed-hq

codspeed-hq Bot commented Aug 19, 2026

Copy link
Copy Markdown

Merging this PR will regress 2 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

⚡ 25 improved benchmarks
❌ 2 regressed benchmarks
✅ 97 untouched benchmarks
🆕 3 new benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
reconcile: deep tree, single deep() effect 79.9 ms 134.6 ms -40.63%
omit 234.6 µs 308.4 µs -23.94%
hasAllowed 54.5 µs 24.6 µs ×2.2
readAllowed 32.4 µs 24.7 µs +30.89%
readAllowed 32.8 µs 25.5 µs +28.59%
readAllowed 32.3 µs 25.2 µs +27.88%
dbmon partial tick — deep reconcile 793.8 ms 626.7 ms +26.66%
dbmon full tick — deep reconcile 766.9 ms 605.6 ms +26.62%
read 34.5 µs 27.4 µs +25.92%
read 34.1 µs 27.3 µs +24.71%
read 36.8 µs 29.7 µs +23.82%
readAllowed 34 µs 27.7 µs +22.61%
reconcile: deep tree, all ~12k paths subscribed 80 ms 65.8 ms +21.64%
construct 39.7 µs 33.7 µs +17.87%
construct 40.1 µs 34.3 µs +17.07%
hasAllowed 26.7 µs 23 µs +16.01%
construct 41.7 µs 36.6 µs +13.92%
construct 62.7 µs 55.2 µs +13.54%
construct 51.8 µs 45.9 µs +12.86%
reconcile: read-once untracked tree, 10 of ~12k paths subscribed 7 ms 6.3 ms +12.23%
... ... ... ... ...

ℹ️ 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 store-rewrite (8190d55) with next (2a49adc)

Open in CodSpeed

ryansolid and others added 26 commits August 18, 2026 22:06
…ship, adoption-channel reconcile

Plain deep stores and reconcile now serve from src/store/next/ per the
INTERNALS-STORE-STATE.md contract (mined from 217 suite-derived rules,
all design rulings closed): one committed home (owned-raw backing, CoW
privatization with path copying — user sources never mutated, pinned by a
__TEST__ oracle), lazy per-property nodes as literal core signals, and a
per-target pending backing as the draft surface. Write channel: drafts
mutate the pending clone natively (array methods just work); setter exit
diffs pb vs committed and setSignals observed keys — write-time
notification with commit deferred to node commit, so transition holds,
per-leaf isPending, and lanes ride core machinery natively (a plain store
write held by downstream async now pends exactly the touched leaves).
Reconcile is the adoption channel: eager backing swap per target,
keyed/positional matching with mode-dependent reachability pruning
(keyed detaches unobserved captures; positional preserves slot identity),
kind changes detach, and the identity skip carries its completed proof
(incoming === backing && !owned) — fixing FINDING-1's unsound
same-reference skip, whose rule test flips to plain `it` here. Snapshot
resolves per-object through the registration (multi-parent/DAG writes
visible through any path). Core gains two treeshakeable seams: a store
commit hook in the scheduler (folds node values into backing at pending
commit) and devGuardStoreSetterWrite (owned-scope write guard moved to
setter entry). Transitional dispatchers route derived/shallow/optimistic
forms to the legacy implementation until their increments land.

Suite: 1227 passing / 26 failing, every failure in a named unported
bucket (shallow/markRaw, affects-on-stores, projection interop, dev
diagnostics, 2 ruled internals). Hot-path gate suites clean. dbmon:
mount/remount/sort beat legacy; tick profiled 23.6→14.4ms/tick across
two optimization rounds (subscription-keys-only fold diff, scan-once
accessor detection). Includes the design doc, rules-mining catalogs,
findings log (3 shipped bugs documented, 1 fixed here), rule-derived
tests, the vite.next gate config, and the store-size/scan-hangs scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
The rewrite now passes the entire suite (91/91 files, 1252 tests; 1
expected fail owned by the future optimistic increment, 2 ruled skips of
legacy-internals pins). Landed in this increment:

- Write path finalized per §3: drafts mutate the pending backing; setter
  exit diffs and setSignals observed keys (write-time notification), so
  transition holds, per-leaf isPending, and lanes ride core machinery —
  a plain store write held by downstream async pends exactly the touched
  leaves. Pending visibility mirrors core read()'s owner-context rule
  (Root→parentComputed, #2687), with the owned-scope write guard moved to
  setter entry (devGuardStoreSetterWrite).
- Legacy interop via structural field aliasing: next targets expose the
  legacy field names (v/n/h/d/s intentionally shared; px/u renames avoid
  the x/p collisions) plus a $PROXY field, so shared machinery — unwrap,
  affects scope walks, witnessAffectsMark, snapshot resolution through
  lookupTarget — reads rewrite targets natively. Cleared the projection
  snapshot and affects buckets wholesale.
- markRaw/shallow interop: raw values never wrap (leaves for reconcile),
  legacy-tracked raws serve their legacy proxy (cross-implementation
  dedupe), shallow's never-both-wrapped-and-raw dev check sees next
  targets. Shallow is NOT a port target (Ryan: perf-only, retirement
  candidate — the dbmon shallow column is its retirement bar).
- Snapshot: owned (written) subtrees copy — non-enumerable symbols
  excluded, copies registered before descent — fixing FINDING-3 (cycle
  identity on written cyclic objects); unowned subtrees keep identity.
- Dev diagnostics: registerGraph on creation, onStoreNodeUpdate full-key
  diff (dev-only), strictRead untracked-read warnings.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ive createProjection bring-up

Families (§7b): projection children wrap into a per-projection registry
(writes land in the projection, never a source family) and every family
node carries the projection computed as its firewall — reads link the
derive's status/lifecycle natively. storeSetterNext extracted as the
low-level write primitive (no wholesale untrack: the writing flag already
prevents draft self-tracking while external signal reads keep tracking —
they ARE the derive's dependencies). Write-override interop honors the
legacy draft flag so post-await async draft writes land with per-op
notification. §6c status gate: an uninitialized async derive throws
NotReady from every trap, probed untracked so the gate never coarsens
isolation. Replace-mode reconcile: projection roots merge entity changes
in place (root proxy stable for life, children not key-matched across the
entity change) and displaced raws unregister.

Two correctness fixes surfaced by bring-up, benefiting all stores:
- notification diff old-side: t.v lags when a recompute runs before the
  prior fold commits (effects run before commitPendingNodes) — swallowed
  writes; the node's own current value is the true old side (setSignal's
  internal equality), pre-compare dropped.
- logical-slot node equality: privatization/adoption change raw identity
  without changing the logical value; node equals now resolves both sides
  through the (family-aware) registration, eliminating phantom notifies
  on container writes after child privatization.

Default suite green (91/91, 1252 passing). Next-native projections pass
basics/selection (27/48 across the projection suites via the gate
config); the default build keeps routing createProjection to legacy until
async/chained bring-up completes.

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

Chained backing works end to end (§7b): a derive returning a live store
proxy adopts the PROXY as backing — reads flow through the inner store's
traps (consumers dual-link inner graph + outer swap nodes; outer node
values never shadow the chain), switching severs via dynamic dependency
rebuild, and snapshot resolves chained backings to the base raw through a
multi-level registration loop. Projection draft writes commit eagerly
(authoritative-landing semantics, matching legacy overlay resolution).
Entity changes swap wholesale (children never matched across an entity
change); same-entity keyed merges preserve child identity UNCONDITIONALLY
in projection mode while plain keyed reconcile keeps the R18 detach of
unobserved captures — identity (re-pointing) and notification depth
(d-gate) are now separate concerns, mode-split per the pinned contracts.
$REFRESH resolves the projection computed from the trap (refresh()/
isPending). Root propagation bug fixed (replace flag now reaches the
descent). Default suite green throughout (91/91).

Gate: projection suites 49/58 (from 27/48) — remaining: the isPending
transition cluster, generator-yield identity policy, Loading-boundary
settle, and two sync stragglers.

Co-authored-by: Cursor <cursoragent@cursor.com>
Projection port complete on the single-home model. Firewall gate links
tracked readers (NotReady settle wake-up, deps drop post-settle so
isolation holds); per-store write scopes extended through draft reads
(legacy Writing semantics — chained projections track dependencies,
cross-store draft writes work); CAS parent-slot fix on fold (draft
splices can't resurrect rows via stale wrap-time indices). RUL-12
unkeyed-merge ruling executed on the two async yield-identity tests.
Gates: projection suites 58/58 on next config, default 91/91 files,
next-gate store sweep 17/17 files (361 tests).

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

Armed nodes ride the engine wholesale — no store-side layer/backup/owner
machinery. Landed: optimistic write channel (visible-view diff, committed
raw untouched), membership overlay on armed has-nodes, optimistic-view pb
seeding + draft reads (#2951 compose), firewall-transition entanglement +
next-shape transitionBlocked half (#2951 hold), landing consumption split
(structural consumes / owned values persist — #2719 + rapid-toggle both
hold), lane-view reconcile baseline (§6b — optimistic rows recycle), §7b
chained-gate pierce for overrides, snapshot/deep optimistic-view compose.
Core fixes: createWriteTraps now save/restores projectionWriteActive
(was hard-reset per op); untracked node-first reads serve the backing for
committed state (O6 read-through). Remaining: one cross-test zombie
cascade (documented in INTERNALS decision log) + affects/status clusters.

Co-authored-by: Cursor <cursoragent@cursor.com>
The affects machinery reads next targets structurally (field names alias
by design) — only node creation dispatches, via injected factories.
lookupTarget handles next family maps (targets, not proxies); the
declaration walk reads pending backings and composes the optimistic view
(injected resolver) so in-motion rows are covered; next nodes inherit
live marks at birth and survive unobserve while marked. Fixed
"authority wins at reveal": ensurePB seeds the optimistic view ONLY for
user drafts — authoritative landings seed committed truth (the override
was folding into the committed home on multi-key post-await landings).
affects-propagation and question-scoped-pending suites fully green.

Co-authored-by: Cursor <cursoragent@cursor.com>
User-context reconcile on optimistic families diffs against the lane view
and emits engine overrides per level: flat slot/membership/length
overrides ride armed nodes (reverting with their transaction), while
key-matched wrappable pairs descend into existing child targets so
captured-proxy identity holds. Committed raw is never touched. The
FINDING-2 rule test (key added by in-window reconcile reverts at settle)
flips from test.fails to passing; reconcile-captured-proxies suite green.

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

Strict-read refetch escalation (PENDING_ASYNC_UNTRACKED_READ on pending
firewalls), witnessAffectsMark resolves chained backings, multi-level
optimistic-view snapshot composition, presence-override structural
classification for landing consumption (post-landing adds consume
correctly), projection folds never eager (downstream holds form later in
the flush; foldHeld + pending-backing visibility give context-free
freshness without breaking hold staleness), ownedWrite store nodes +
root-exempt setter guard, live-override-gated transitionBlocked half.
Default suite: 1251/1255 — remaining 4 are one in-file zombie cascade
(all pass in isolation; unhandled-rejection leak documented in the
decision log as next cycle's first item).

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

91/91 files, 1253 passed, zero unhandled errors; next-gate 362 passed.
(1) §6 length-as-view on optimistic arrays: length serves the composed
view (backing ± overrides), node kept for tracking only — the torn
iteration class (length on the stale rail, indices on pending) is
impossible by construction. (2) Landing consumption performs the full
legacy node reset (fold committed into _value, clear pending) — a parked
transaction's stashed queues stranded subscriber wakes otherwise.
(3) $TRACK on chained backings chains to the inner key-set node (§7b
structural chaining, #2864): mapArray over wrapper views observes source
structural notifications.

Co-authored-by: Cursor <cursoragent@cursor.com>
Plain-store adoption notifies inline after the reconcile descent (the
reconcile diff IS the fold diff — no foldOlds queue/drain round trip);
projections keep deferred folds for downstream-hold semantics. Suite
stays green (1253). Type fixes restore `pnpm build`: overload placement
in legacy optimistic.ts, Computed<void> shape in next/optimistic.ts,
ownKeys cast. Perf checkpoint + profile attribution recorded in the
decision log (single-sweep numbers, ABBA required before claims).

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

scanAccessors (full descriptor enumeration per object on first read —
115.7ms self in the uibench profile, the largest store cost) is deleted.
Accessor-ness is a per-node creation-time flag via allocation-free
own-gated __lookupGetter__/__lookupSetter__ probes; accessor keys serve
through Reflect.get with the proxy receiver (R20-correct); folds use the
cached flag + one own-gated incoming-side probe (merge-installed getters
pinned by suite; prototype getters keep invoke-compare, also pinned).
createTarget assigns fields directly on a shared hidden-class chain.
uibench: legacy 36.6 → next 27.5 (25% faster; keyed ops 0.67-0.85x).
dbmon tick: 14.35 → 11.50ms — legacy parity. Suite green (1253).

Co-authored-by: Cursor <cursoragent@cursor.com>
Cached chained-backing flag (no per-read symbol probe), node lookup
threaded into serveDataKey (was doubled), interned-string pollution
guard, fam-first gate ordering, descend consults legacy interop only on
next-lookup miss, object-branch adoption iterates for-in + conditional
symbol pass. Suite green (1253). ABBA rounds pin the remaining dbmon
delta at ~1.3x with a single structural cause documented in the decision
log: the adoption channel's second diff (notifyFold re-walk) vs legacy's
fused applyStateFast — the fuse is the next scoped item.

Co-authored-by: Cursor <cursoragent@cursor.com>
Size script gains next-only entries (dispatchers bypassed) and a FLOOR
variant with legacy modules stubbed. Full-featured next store (plain +
projections + optimistic + reconcile + snapshot/deep) floors at 12.8kb
gz total = ~4.4kb store attribution vs shipped's 15.5kb — 3.5x smaller
with capabilities 1.9 never had. As-is next-only (19.5kb) shows interop
imports drag legacy wholesale: the gap is the deletion worklist (port
derived createStore form, move createWriteTraps, then delete legacy).

Co-authored-by: Cursor <cursoragent@cursor.com>
createStore(fn, seed) routes to projection internals with a
recompute-masking setter (core R31). The §6c gate covers errored derives
and reduces to the raw-fallthrough guard: tracked reads link through
firewall-backed nodes (core read links-then-throws — the node link wakes
async-memo readers on landings, legacy parity; a pre-linking gate broke
Loading boundaries and was discarded). Has-/key-set nodes carry the
firewall arg. Eager folds split by channel: sync-derive drafts defer,
post-await landings commit immediately — both spec-async pins hold.
Suite green (1253). All non-shallow paths now serve from the rewrite.

Co-authored-by: Cursor <cursoragent@cursor.com>
applyAdopt notifies inline per key in both object and array branches
(descend-then-notify ordering preserves R9 slot identity silence);
deleted keys via counted fast-out on a live node count; shared
notifyKeyDiff/notifyFoldTail helpers; direct value writes (no closure
per changed key). notifyFold remains for deferred projection folds only.
Suite + next-gate green. dbmon next/legacy ≈ 1.2-1.3x (from ~1.35);
cool-machine ABBA owed before the shallow ruling — inputs recorded in
the decision log.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nnel-split probes

Tracked node reads in serveDataKey use read()'s hoisted plain-signal
fast path (legacy trap parity); the fused walk pre-checks wrappability
inline so scalar keys skip the descend call; accessor probes are
setter-channel only (reconcile adopts immutable data by contract, R2a —
all pinned getter-preservation tests are setter-channel; suite green
confirms). Suite 1253 green.

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

Per-key notify body inlined into the fused walk (no helper call per
key); reference-identity early-continue with the ownership guard (the
sound form of legacy's FINDING-1 skip); values fetched once per side.
New in-process A/B bench interleaves next and legacy in one vitest run
— full-tick parity confirmed (0.98x mean), partial residual ~7-10%.
Suite green (1253). Journal updated.

Co-authored-by: Cursor <cursoragent@cursor.com>
Default vitest defines __TEST__ true: next pays per-adoption invariant
oracles legacy lacks — prior A/B overstated next. In benchmark mode
full tick oscillates around parity (0.81-1.06x across runs), partial
1.0-1.14x; cross-run variance now exceeds effect size. Measurement
protocol recorded: benchmark mode only, cool machine for the verdict.

Co-authored-by: Cursor <cursoragent@cursor.com>
Symbol compares gated behind one typeof on the hot string path; common
serve case inlined (readNodeFast, no helper frame, no FORCE compare for
plain nodes, primitive bail before isWrappable). Keyed arrays walk a
positional prefix in place (identity skip inline with the FINDING-1
ownership guard) and only build prevByKey for a misaligned remainder —
aligned polling ticks never allocate the map. Browser dbmon partial
closed from an order-robust 1.24x to 0.97-1.03x; full tick in the
parity band. Suite + next-gate green. Sweep-order bias documented.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stage 1 validated deep cannot reach shallow-class tick perf (phase-2
edit-script question; retirement may reopen there) — shallow stays and
ports: t.s targets serve children verbatim (proxies by reference,
#2932), sticky raw-marking at creation/set/adoption (shared R41
invariant), slot-granular positional reconcile with descends gated off.
Dispatcher routes plain shallow to next. Suite green (1253) incl the
#2932 suite; in-process A/B parity with legacy shallow (15.88 vs
15.48ms mean, p75 favors next). Remaining for wholesale deletion:
fam.shallow forms, createWriteTraps move, legacy+dispatch delete.

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

fam.shallow wires shallow projections and optimistic stores to the same
t.s slot semantics; the serve rule matches #2932 exactly (raw-marked
data verbatim, store-proxy slots get boundary wrappers in the shallow
family for chain write isolation). Dispatcher shallow routes removed.
Suite green (1253) — every public store form now serves from the
rewrite; legacy is unreachable except shared utilities and
createWriteTraps. Deletion is next.

Co-authored-by: Cursor <cursoragent@cursor.com>
The rewrite now serves every public store form, so legacy goes:
reconcile/projection/optimistic modules removed, store.ts gutted to
shared machinery (~1600 → ~570 lines: symbols, raw-marking,
isWrappable, write-override, next-only affects walk, StoreNode as a
structural view of StoreNextTarget), utils.ts to merge/omit, the
transitional dispatchers and test shims removed — store/index exports
next directly. createWriteTraps lives in next/projection; next's
optimistic hooks are self-contained (engine + own clear + blocked
half). Orphan sweep: storeLookup, symbolKeyedRecords (+ skipped
legacy-pinned test), registerTransientStoreNode, mergedOverlay.
Suite 1253 green; full monorepo build + tests green; in-process dbmon
bench in family. Size vs main (gzip): full 24162 → 23259 (−3.7%),
store attribution 15572 → 14803.

Co-authored-by: Cursor <cursoragent@cursor.com>
reconcile and setter writes store function leaves by identity, never
invoking them — the rewrite is immune by construction (every node write
is thunk-wrapped through setSignal). Optimistic case pins that function
leaves behave identically to scalars: draft serves by identity, never
invoked, ambient write reverts at settle. Tests only — no changeset.

Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid and others added 7 commits August 18, 2026 22:06
Interleaved thermal-fair A/B (sign test over alternating samples)
exposed a +1.4ms (~10%) dbmon tick regression that block-ordered and
dev-mode benches masked: raw-as-truth nodes serve raw values, so every
tracked object read paid wrapNext's WeakMap lookup + isWrappable where
legacy nodes stored pre-wrapped values. Nodes now cache the served
proxy + the raw it wrapped (px/pxv); a pointer compare replaces both
costs, a replaced child fails the compare and re-wraps. Measured:
tick +1.4ms → +0.3-0.5ms, tickPartial parity, sort now WINS (68/23).
Negative result kept as a comment: notifyKeyValue's pre-compare is
load-bearing (setSignal parks pending before commit-time equality,
RUL-1) — removing it made identity-preserved adopted containers pay
full write machinery (93/5 partial-tick regression, reverted).

Co-authored-by: Cursor <cursoragent@cursor.com>
descend resolves the tracked target FIRST: a lookup hit implies the old
side was wrappable and never raw-marked (only wrappables acquire
targets), deleting per-pair isWrappable/isRawValue on that side; the
new side still validates fully (frozen/platform/markRaw'd values stay
leaves, R42). The keyed walk's prefix/remainder alignment checks are
routing heuristics — both routes notify identically — so they drop to
typeof gates with descend as the one validator. isWrappable left the
hot profile. Interleaved n=100 ×3: tick +0.0/+0.3/+0.4ms (statistical
parity), partial parity, sort next-wins every run; block sweep tick
median next-faster (12.3 vs 12.8, best 10.4 vs 11.5). Suite green.

Co-authored-by: Cursor <cursoragent@cursor.com>
Moves the optimistic-only machinery (notifyOptimisticWrites,
consumeOverridesNext, optimisticView, applyTentative) out of the plain
store/reconcile modules into next/optimistic.ts behind an injection
table installed in createOptimisticStore's once-guard (no module-scope
side effects — the module shakes). Sound by construction: every call
site is fam?.opt-gated and optimistic families are only mintable by
createOptimisticStore. Dead-export sweep rides along. Trade-off ruled
(Ryan): plain store entry −481 gzip (13710 → 13229), full-import
bundle +181 (headline eaten); package remains −722 vs main. Perf
guards: dbmon tick next-wins 41/16, optimistic write-storm 0.94x,
suite + full monorepo green.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rebase onto next brought effect-phase-writes coverage: a store write
inside onSettled must park (immediate read returns the settled value,
signal parity). Core's read() gained the rule in #3006 — CHILDREN_
FORBIDDEN execution scopes get committed visibility — but the store's
inOwnerContext() mirror predated it. Now checks the resolved owner's
config flag exactly like core. Suite green (1282).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…es (same public contract, new implementation)

Co-authored-by: Cursor <cursoragent@cursor.com>
+createStore 13.5 -> 12.2 KB (measured 11.98 — legacy deletion +
tree-shakeable optimistic channel took −1.26 KB out of the scenario);
hydrating+stores 23.65 -> 23.3 KB (measured 22.84). Both at the ~2%
headroom convention the config documents. Tooling only — no changeset.

Co-authored-by: Cursor <cursoragent@cursor.com>
CodSpeed flagged 2 regressions on the PR. omit-mixed(100,25) reproduced
at parity locally (0.6%) — CodSpeed environment noise per its own
runtime-environment warning. The deep() one was real (+43% local):
deepNext created/read a node PER PATH on every effect re-run where
legacy subscribed one $TRACK per record. Targets now carry a lazy
deep-witness node (dk): deep() reads key-set + witness per record and
walks targets directly (no per-child proxy round-trip); the adoption
walk, setter notify, fold, and optimistic channels bump dk only when
it exists — value changes on un-noded keys included, in-place
adoptions excluded (targetsEqual after descend). affects() declaration
walks mark k/dk like property nodes and dk inherits live marks at
birth, keeping isPending-over-deep() probes working (3 optimistic
suite pins). Bench: 21.7ms → 15.18ms vs next's 15.24ms — parity; the
sibling rows stay branch-favored (+21%/+9%); dbmon in family. Suite
1282 green; full monorepo build + tests green.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 0797215 into next Aug 19, 2026
5 of 8 checks passed
brenelz added a commit to brenelz/solid that referenced this pull request Aug 19, 2026
The store rewrite (solidjs#3019) replaced the legacy store with the
store/next implementation; the attribution-gated store node naming
moves from the legacy getNode to store/next getNode (same semantics:
name only when the engine is installed, one null check otherwise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants