Store rewrite: single-home storage model replaces the legacy store - #3019
Conversation
🦋 Changeset detectedLatest commit: 07d06df The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 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 |
size-limit report 📦
|
Coverage Report for CI Build 32223705775Warning No base build found for commit Coverage: 72.158%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
Merging this PR will regress 2 benchmarks
|
| 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)
…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>
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>
9bba377 to
8190d55
Compare
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>
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>
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.tsreduced to shared machinery and no transitional dispatchers.Performance (vs shipped
next, prod builds)Size
next(bundlephobia-style, 22,845 vs 23,367)createOptimisticStore; all call sitesfam?.opt-gated)Fixes shipped bugs
snapshotbreaking cycle identity on written cyclic objects (FINDING-3)Test plan
Made with Cursor