Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8af8f2f
feat(signals): store rewrite phase 1 — single-home storage, CoW owner…
ryansolid Aug 18, 2026
3dd8273
feat(signals): store rewrite — full plain-store suite parity
ryansolid Aug 18, 2026
a553892
feat(signals): store rewrite — projection infrastructure and next-nat…
ryansolid Aug 18, 2026
d3d7e2e
feat(signals): store rewrite — projection bring-up round 2 (49/58 on …
ryansolid Aug 18, 2026
9c80b10
store rewrite: next-native projections default; all suites green
ryansolid Aug 18, 2026
fa7cb08
store rewrite: optimistic stores on core lanes (increment, 64/68 main…
ryansolid Aug 18, 2026
73b1238
store rewrite: affects/marks integration + authoritative-seed fix
ryansolid Aug 18, 2026
e6917d9
store rewrite: tentative reconcile channel (§6b) — FINDING-2 fixed
ryansolid Aug 18, 2026
6b687c4
store rewrite: optimistic surface completion — all genuine failures f…
ryansolid Aug 18, 2026
4740532
store rewrite: full suite green — zombie decomposed into three real f…
ryansolid Aug 18, 2026
ce2d1d4
store rewrite: eager plain-store adoption + worktree build fixes
ryansolid Aug 18, 2026
0134740
chore: changeset for the eager-adoption commit
ryansolid Aug 18, 2026
8ec8dc0
store rewrite perf: kill eager accessor scans + direct target constru…
ryansolid Aug 18, 2026
cfe4784
store rewrite perf: hot read-path cuts + dbmon gap decomposition
ryansolid Aug 18, 2026
0ee2041
store rewrite: initial size audit — floor at ~4.4kb gz attribution
ryansolid Aug 18, 2026
b18a400
store rewrite: derived createStore form on next + gate restructure
ryansolid Aug 18, 2026
75095bc
store rewrite perf: fused single-pass adoption walk
ryansolid Aug 18, 2026
cc67e92
store rewrite perf: readNodeFast in traps + scalar descend skip + cha…
ryansolid Aug 18, 2026
39229de
docs: store-rewrite performance baselines + open tick regression
ryansolid Aug 18, 2026
e2e63c5
store rewrite perf: inline per-key walk + guarded identity skip + A/B…
ryansolid Aug 18, 2026
cd1ba5f
docs: A/B benchmark-mode findings — full-tick in parity band
ryansolid Aug 18, 2026
3c12d31
store rewrite perf: trap read flattening + positional-prefix keyed walk
ryansolid Aug 18, 2026
9f90eb3
store rewrite: shallow stores ported to next (O4 re-ruled two-stage)
ryansolid Aug 18, 2026
52a8622
store rewrite: shallow derived/optimistic forms on next — porting com…
ryansolid Aug 18, 2026
6566b86
store rewrite: delete the legacy implementation
ryansolid Aug 18, 2026
462a185
test: pin #3017 function-leaf semantics against the rewrite
ryansolid Aug 18, 2026
9217551
store rewrite perf: per-node wrap cache closes the prod tick regression
ryansolid Aug 18, 2026
aecb23d
store rewrite perf: single-authority walk validation — dbmon tick parity
ryansolid Aug 19, 2026
9e1621f
store size: treeshakeable optimistic channel + dead-export sweep
ryansolid Aug 19, 2026
b1d7ee8
store rewrite: mirror core's #3006 committed-visibility rule
ryansolid Aug 19, 2026
f4b6243
changeset: settled-visibility fix (rides 4c762e84)
ryansolid Aug 19, 2026
527ee5e
changesets: store-rewrite entries are patches — no breaking API chang…
ryansolid Aug 19, 2026
8190d55
size: ratchet store scenarios to lock in the rewrite's win
ryansolid Aug 19, 2026
07d06df
store: deep() witness node — close the single-deep()-effect regression
ryansolid Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/store-deep-witness-node.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@solidjs/signals": patch
---

Store: `deep()` subscribes one witness node per record instead of one node
per path. The rewrite's first `deep()` created and read a property node for
every reachable key on every effect re-run (~40% regression on the
single-deep()-effect benchmark vs the legacy per-record $TRACK). Targets now
carry a lazy deep-witness node: `deep()` reads the key-set node plus the
witness per record and walks targets directly (no per-child proxy
round-trips); write channels bump the witness only when it exists (one null
check otherwise). Declared affects() scopes mark the witness like any
property node, so isPending() probes over deep() reads keep working.
Benchmark restored to parity with the legacy implementation.
8 changes: 8 additions & 0 deletions .changeset/store-next-settled-visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@solidjs/signals": patch
---

Store: pending-value visibility in the rewrite mirrors core's #3006 rule —
CHILDREN_FORBIDDEN execution scopes (createTrackedEffect / onSettled
callbacks) read committed values, so a store write inside onSettled parks
and an immediate read returns the settled value, matching signals.
9 changes: 9 additions & 0 deletions .changeset/store-rewrite-creation-perf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@solidjs/signals": patch
---

Store rewrite performance: eliminate the eager per-object accessor scan
(replaced by allocation-free per-node probes computed at node creation, with
own-gated single probes on fold paths), and construct proxy targets with
direct field assignment on a shared hidden-class chain. uibench drops from
36.6ms (legacy) to 27.5ms; dbmon tick reaches legacy parity.
12 changes: 12 additions & 0 deletions .changeset/store-rewrite-delete-legacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@solidjs/signals": patch
---

Store rewrite: legacy implementation deleted. The store module now has a
single implementation — the rewrite's single-home storage model (raw as
truth, CoW pending backings, lazy per-property nodes, adoption-channel
reconcile) serves every public form: plain, shallow, derived, projection,
and optimistic stores. `store.ts` is reduced to shared machinery (symbols,
raw-marking, wrappability, affects scopes), the transitional dispatchers are
gone, and the package is ~0.9kb gzip smaller than before the rewrite while
carrying the same contract.
10 changes: 10 additions & 0 deletions .changeset/store-rewrite-derived-form.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@solidjs/signals": patch
---

Store rewrite: the derived writable `createStore(fn, seed)` form serves from
the rewrite (projection internals + a recompute-masking setter). The §6c
status gate now covers errored derives (memo parity) and only guards raw
fallthrough — tracked reads link through firewall-backed nodes in core
read(), so landings wake async-memo readers exactly like legacy. Has- and
key-set nodes carry the firewall link too.
7 changes: 7 additions & 0 deletions .changeset/store-rewrite-eager-adoption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@solidjs/signals": patch
---

Store rewrite: plain-store reconcile notifies inline after the adoption
descent instead of a queue/drain round trip; projection folds stay deferred
for downstream-hold correctness.
9 changes: 9 additions & 0 deletions .changeset/store-rewrite-fused-adoption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@solidjs/signals": patch
---

Store rewrite performance: the adoption walk notifies inline (fused single
pass — descend first so identity-preserved slots stay silent, then per-key
node notification), replacing the separate fold re-walk. Deleted keys ride a
counted fast-out; presence/membership stay as a shared tail. Fold writes pass
values directly instead of allocating a closure per changed key.
11 changes: 11 additions & 0 deletions .changeset/store-rewrite-node-wrap-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@solidjs/signals": patch
---

Store rewrite: per-node wrap cache on the read path. Raw-as-truth stores raw
values in nodes, so every tracked object read paid a WeakMap lookup to
re-wrap the child — measured as a ~10% dbmon tick regression vs the legacy
implementation, whose nodes stored pre-wrapped values. Nodes now cache the
last served proxy and the raw it wrapped; one pointer compare replaces the
WeakMap hit and the wrappability check. A replaced child fails the compare
and re-wraps, so no invalidation hooks are needed.
11 changes: 11 additions & 0 deletions .changeset/store-rewrite-optimistic-completion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@solidjs/signals": patch
---

Store rewrite: optimistic surface completion. Strict-read refetch-window
escalation, marks witnessing through chained store views, snapshot composition
across chained optimistic targets, presence-based structural classification for
landing consumption, hold-correct projection backing folds (never eager;
context-free freshness via pending-backing visibility), root-exempt store
setter guard with ownedWrite store nodes, and a transitionBlocked store-half
that only holds transactions carrying live overrides.
15 changes: 15 additions & 0 deletions .changeset/store-rewrite-optimistic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@solidjs/signals": patch
---

Store rewrite: optimistic stores ride core lanes natively. `createOptimisticStore`
(plain and derived, non-shallow) now serves from the rewrite: per-property nodes
are armed core signals, so optimistic writes, per-transaction ownership,
entanglement, reverts, and refetch-holds are all engine-inherited — the
store-side override layer, backup snapshots, and owner maps are gone. Structural
optimism (adds/deletes/length) rides armed presence nodes and consumes on
authoritative landings; value overrides persist with their owning transaction.
Reconcile diffs against the lane view so optimistic rows recycle their proxies
when landed data carries the same key. snapshot/deep compose the optimistic
view. Also fixes legacy `createWriteTraps` clobbering `projectionWriteActive`
(hard reset instead of save/restore).
12 changes: 12 additions & 0 deletions .changeset/store-rewrite-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@solidjs/signals": patch
---

Store rewrite: full plain-store parity. The rewrite now passes the entire
suite (91/91 files) serving plain deep stores and reconcile package-wide:
transition holds (write-time notification via per-key nodes, per-leaf
isPending), affects() coverage over rewrite targets, legacy interop via
structural field aliasing (v/n/h/d/s + $PROXY), markRaw/shallow interop,
dev diagnostics (registerGraph, onStoreNodeUpdate, strictRead warnings), and
owned-subtree snapshot copies (non-enumerable symbols excluded, cycle
identity preserved — fixes FINDING-3's cycle duplication).
16 changes: 16 additions & 0 deletions .changeset/store-rewrite-phase-one.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@solidjs/signals": patch
---

Store rewrite phase 1 (in progress, branch-scoped): plain deep stores and
reconcile now serve from the new single-home storage model
(`src/store/next/`) — owned-raw backing with copy-on-write privatization
(user source objects are never mutated), lazy per-property nodes as real core
signals (transition holds, isPending, and lanes ride core machinery
natively), and reconcile as the adoption channel with the ownership-guarded
identity skip. Fixes the unsound nested reconcile same-reference skip
(FINDING-1: a re-sent reference after a flushed setter write now restores the
incoming values). Derived, shallow, and optimistic store forms still route to
the legacy implementation via transitional dispatchers. Design contract and
findings log: `packages/solid-signals/INTERNALS-STORE-STATE.md`,
`packages/solid-signals/rules-mining/`.
17 changes: 17 additions & 0 deletions .changeset/store-rewrite-projection-infra.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@solidjs/signals": patch
---

Store rewrite: projection infrastructure. Family wrapping (per-projection
child registries with firewall-carrying nodes), the storeSetterNext
primitive, write-override interop for post-await draft writes, the §6c
status gate (uninitialized async derives are unobservable through every
trap), replace-mode reconcile (projection roots merge entity changes in
place, displaced raws unregister), and a next-native createProjection
(bring-up: passes basics/selection; async and chained suites gate via the
next config while the default build keeps routing projections to legacy).
Also two correctness fixes benefiting all stores: the write-notification
diff no longer uses a lagging old-side (a recompute before the prior fold
commits could swallow changes), and node equality is logical-slot aware
(privatization/adoption raw-identity swaps no longer produce phantom
notifications).
11 changes: 11 additions & 0 deletions .changeset/store-rewrite-projections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@solidjs/signals": patch
---

Store rewrite: next-native projections are now the default. `createProjection`
(sync, async, generator, and chained forms) runs on the single-home storage
model — firewall status gating links tracked readers for NotReady wake-up,
write scope extends through draft reads (cross-store draft writes preserved),
and fold-time parent-slot fixes are compare-and-swap so draft array splices
cannot resurrect removed rows. Unkeyed nested objects in async yields now
merge in place (identity preserved) instead of accidentally replacing.
10 changes: 10 additions & 0 deletions .changeset/store-rewrite-shallow-forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@solidjs/signals": patch
---

Store rewrite: shallow derived and optimistic forms serve from the rewrite
(fam.shallow → root slot semantics). The shallow serve rule is now exact
legacy parity (#2932): raw-marked data serves verbatim, store-proxy slot
values get boundary wrappers in the shallow store's own family so downstream
writes never land upstream. Every public store form now runs on the new
implementation.
10 changes: 10 additions & 0 deletions .changeset/store-rewrite-shallow-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@solidjs/signals": patch
---

Store rewrite: shallow stores serve from the rewrite. Slot values are stored
verbatim (proxies pass through by reference, #2932) and served raw; ingest is
sticky raw-marked at creation, set-trap, and reconcile adoption (the
never-both-wrapped-and-raw invariant); reconcile on shallow targets is the
slot-granular positional diff with no descent. In-process A/B holds legacy
shallow's dbmon performance at parity.
10 changes: 10 additions & 0 deletions .changeset/store-rewrite-suite-green.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@solidjs/signals": patch
---

Store rewrite: full suite green. Optimistic array length is a view of the
composed membership (tear-free iteration by construction), landing consumption
folds committed values into nodes directly (no stranded wakes behind parked
transactions), and `$TRACK` on chained store views reads through to the inner
store's key-set node so keyed iteration observes structural changes at the
source (#2864).
11 changes: 11 additions & 0 deletions .changeset/store-rewrite-tentative-reconcile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@solidjs/signals": patch
---

Store rewrite: tentative reconcile channel and affects integration. A
user-context reconcile on an optimistic store now parks as engine overrides
(values, membership, length) instead of committed adoption — key-matched rows
keep proxy identity via descent, and everything reverts with its transaction.
Fixes FINDING-2 (a key added by an in-window reconcile now reverts at settle).
The affects/marks system covers next-store targets, including optimistic rows
in motion at declaration time.
13 changes: 13 additions & 0 deletions .changeset/store-rewrite-walk-validation-hoist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@solidjs/signals": patch
---

Store rewrite: reconcile walk validation hoisted to one authority. `descend`
resolves the tracked target first — a lookup hit implies the previous value
was wrappable and never raw-marked (only wrappables acquire targets), so
per-pair `isWrappable`/`isRawValue` checks on the old side are gone; the new
side still validates fully (frozen/platform/markRaw'd values stay leaves).
The keyed array walk's alignment checks are routing heuristics, not
semantics, so they use bare typeof gates and defer validation to `descend`.
Closes the remaining dbmon tick gap to statistical parity with the legacy
implementation (best-case ticks now beat legacy's).
13 changes: 13 additions & 0 deletions .changeset/store-treeshakeable-optimism.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@solidjs/signals": patch
---

Store: the optimistic write channel is tree-shakeable. The optimistic-only
machinery (engine-write diffing, landing consumption, view composition, the
tentative reconcile channel) moved out of the plain store/reconcile modules
into the optimistic module behind an injection table installed by
`createOptimisticStore` — every call site is `fam?.opt`-gated, so the table
is always populated before it can be reached and plain paths pay nothing.
Apps that never use optimistic stores now shake ~0.5kb gzip of store code;
the whole-package (bundlephobia-style) size drops ~0.5kb gzip as well from
the accompanying dead-export sweep.
130 changes: 130 additions & 0 deletions documentation/performance-experiments.md
Original file line number Diff line number Diff line change
Expand Up @@ -6723,3 +6723,133 @@ Read:
- The extra branch/counter/marker state in the hot loop appears to cost more than the saved generic `formatChildId` work.
- Reverted the source probe. The safe runtime baseline remains Investigation 21 + 22, without this id-cache change.


## Store Rewrite Lane (2026-08-18): baselines at full functionality

The store rewrite (`src/store/next/`, branch `store-rewrite`; contract in
`packages/solid-signals/INTERNALS-STORE-STATE.md`) reached full-suite green
plus one optimization pass today. Baselines below; per-scenario variance
matters more than totals.

### Tier-1 (in-repo, `pnpm vitest bench --run tests/store/...`)

Legacy = main checkout, next = worktree, same machine back-to-back. The
dbmon SHALLOW row runs identical legacy code in both checkouts — it is a
built-in noise control (measured 1.14x session skew for this pair; treat
sub-15% Tier-1 deltas accordingly).

| bench (mean) | legacy | next | ratio |
|---|---|---|---|
| reconcile read-once tree, 10 of ~12k paths subscribed | 0.87ms | 0.51ms | 0.59x |
| tree reverse 1111 keyed | 4.55 | 4.19 | 0.92x |
| tree shuffle 1111 keyed | 4.47 | 4.74 | 1.06x (rme ±9–11%) |
| dbmon full tick deep | 121.4 | 133.4 | 1.10x |
| dbmon partial tick deep | 126.2 | 137.7 | 1.09x |
| dbmon shallow (control) | 66.2 | 75.2 | 1.14x |

### Tier-2 (browser)

- UIBench (matched morning session, 10-iter): legacy 36.6ms total → next
27.5ms. Per-test: ZERO regressions >1.1x&0.05ms; wins concentrate in
keyed structure (moves 0.62x, sort/filter 0.71x) and creation
(tree/render 0.68x). vs ivi (21.4): we now beat ivi on table/filter;
remaining gaps are tree/render (creation) and reorders (re-derivation
stack — phase-2 signature).
- dbmon browser (same-session 30-iter, all columns together): sort 0.92x
(faster than legacy), mount 1.03x, remount 1.07x, unmount noise — but
tick 1.21x and tick_partial ~1.2x SLOWER.

### The open regression (the current target)

Both tiers agree in direction: dense value-diff reconcile (dbmon full and
partial tick) is slower than legacy — Tier-1 shows ~1.10x raw (inside the
1.14x control band, so partially environmental, but Tier-2's same-session
1.21x confirms a real gap). Everything else is parity or faster. The
requirement is parity (no regression) before phase 2. Suspects after the
fused-walk pass: per-target adoption bookkeeping (registration WeakMap.set,
ownership WeakSet.has), per-key call overhead in the fused walk
(notifyKeyDiff + setSignal per changed key) vs legacy's monolithic
applyStateFast. Iteration tool: `reconcile-dbmon.bench.ts` (sub-10s loop,
shallow row as control).

### Inline per-key pass + in-process A/B (2026-08-18, later)

Changes: per-key notify body inlined into the fused adoption walk (legacy's
own lesson — extracted helper cost ~7% on CodSpeed); reference-identity
early-continue per key with the FINDING-1 ownership guard (`ov === nv` skips
only unowned backings; accessor-flagged nodes never skip); both-side fetches
done once. Suite green.

New tool: `tests/store/reconcile-dbmon-ab.bench.ts` — the same workload
against next (dispatcher) and legacy (direct import) interleaved in ONE
process. No session/thermal skew; this supersedes cross-checkout Tier-1
comparisons for the transition period. Delete with the legacy modules.

Results (time:4000, both variants same process):

| bench | next | legacy | read |
|---|---|---|---|
| full tick mean | 187.0 | 190.7 | parity (0.98x) |
| full tick min | 146.9 | 133.4 | 1.10x, GC-riddled (rme ±20%) |
| partial mean | 148.9 | 134.4 | 1.11x (rme ±6–12%) |
| partial min | 124.2 | 116.4 | 1.07x |

Verdict: full-tick parity reached in-process; partial holds a residual
~7–10% (borderline vs rme but consistently legacy-favored). Remaining
partial suspects: per-child adoption bookkeeping on value-identical rows
(registration WeakMap.set + adopted-flag bookkeeping per query array/row
even when every key ===-continues). Browser re-validation owed on a cool
machine with the current build.

### A/B corrected for __TEST__ machinery (benchmark mode)

Discovery: default vitest runs define `__TEST__: true`, which makes next pay
its invariant oracles (ingestedRaw WeakSet add PER ADOPTION, no-mutation
assertions on privatize/fold paths) that legacy has no equivalent of — the
earlier A/B numbers overstated next's cost. `--mode benchmark` strips them
(vite.config.ts already provided the mode).

Two benchmark-mode runs, same command minutes apart (means):

| bench | run 1 | run 2 |
|---|---|---|
| full tick next/legacy | 129.5/160.8 = **0.81x** | 139.8/132.0 = 1.06x |
| partial next/legacy | 140.4/131.2 = 1.07x | 155.5/136.8 = 1.14x |
| partial MINS | 115.5/114.8 = 1.01x | 128.7/117.5 = 1.10x |

Verdict: full-tick oscillates AROUND parity (0.81–1.06x) — no measurable
regression remains, and no stable win either; partial reads 1.0–1.14x.
Cross-run variance (bench order, GC epochs, end-of-day thermals) now
exceeds the effect size even in-process. STOP MEASURING HERE. Definitive
read = cool machine, first runs of the day, both A/B runs repeated 3x,
report min-of-means per side. Measurement rule going forward: any
next-vs-legacy claim must come from `--mode benchmark` (the __TEST__
asymmetry biases default-mode numbers against next).

### Read-path flattening + positional-prefix keyed walk (2026-08-18, midday)

Two more single-loop changes (no forks):
1. Trap read path: one `typeof key` gates all brand-symbol compares off the
hot string path; the common serve case (existing plain node, unchained,
tracked) is inlined in the trap — readNodeFast, no serveDataKey frame, no
FORCE compare (only accessor keys hold the sentinel), primitives bail
before isWrappable.
2. Keyed arrays: positional-prefix fast path (legacy keyedMatch-walk
parity) — aligned rows descend in place with inline identity skip
(FINDING-1 guard); prevByKey is built only for the misaligned remainder,
never on aligned ticks (was: 1000-entry Map per tick).

Browser (octane harness, alternating rounds, current build):
- FULL tick: 1.02/1.08/1.15x pre-prefix → 1.10/1.04x post — parity band.
- PARTIAL tick: was ~1.24x IN BOTH SWEEP ORDERS (the one order-robust
regression) → 1.03x / 0.97x post-prefix — CLOSED.
- Sweep-order finding: run.mjs's fixed order (solid before solid-next)
biases the second column; reversed-order run flipped full tick from
1.17x to 1.045x. Alternating per-round loops are the trustworthy method;
medians from single fixed-order sweeps are not.
- Suite + next-gate green throughout.

Status vs the no-regression bar: full and partial tick both in the
alternating-measurement parity band (0.97–1.15x swings, centered ~1.05);
sort/mount/unmount at parity or faster. Cool-machine confirmation still
recommended for the record.
Loading
Loading