You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On solid-js@2.0.0-rc.7 (browser build), an effect that tracks a createOptimisticStore view through deep() never re-runs when the base store it derives from is written. A per-key read on the same view row re-runs fine, and reading the value afterwards shows the new data — so the write lands and the view serves it, but deep() never hears about it.
This holds for both shapes — deep(view[0]) on a row proxy and deep(view) on the root — and needs no action, no optimistic write, and no in-flight transaction. Just a plain base write with the optimistic view sitting there.
Works on rc.0. Broken from rc.1 onward (verified with @solidjs/signals pinned to rc.1), which lines up with the store rewrite landing in rc.1 (0797215, "deep() subscribes one witness node per record").
The same deep(row) effect on a plaincreateStore row (no optimistic view) re-runs on rc.7 as expected, so this is specific to the optimistic-view composition.
Swapping setBase for an optimistic setView write does wake deep(view[0]) on rc.7 (it fires for the write and again for the revert). So the deep witness sees writes that go through the view's own family, but not writes that arrive from the base family it composes over.
Where it looks like it goes wrong (from the rc.7 @solidjs/signals source, src/store/next/store.ts)
deep() → deepNext() walks targets directly and subscribes two nodes per record: getKeySetNode(t) and the lazy deep-witness getDeepNode(t) (target.dk). The child keys come from Reflect.ownKeys(readSource(t)), so no per-key nodes get linked on the way down.
Write paths bump the witness via bumpDeep(t) — in the setter's notify (written-keys diff), notifyFold, and the reconcile walk. Those bumps land on the target that was written: the base family's target.
A derived optimistic view's targets are a separate family with a chained backing (t.ch, §7b). Per-key reads survive this because serveDataKey links through the chain. The $TRACK get-trap path also handles it explicitly: "a chained backing's $TRACK reads through to the INNER store's key-set" (2.0.0-beta-16 | optimistic update is not being rolledback when rendered from a derived store and a <For> #2864 / core R21, readSource(target)[$TRACK]).
deepNext's walkT has no equivalent read-through: it reads the view target's k and dk only. Nothing that happens on the base family ever touches those two nodes, so a base write is invisible to a deep() subscriber on the view — while an optimistic write (which goes through the view family's own setter) bumps them and fires.
The $TRACK read-through rule looks like the closest precedent: walkT has no equivalent for the inner target's key-set and deep-witness nodes when t.ch is set.
The question
By design? If deep() over an optimistic view is meant to be a view-family-only subscription and base changes are expected to be tracked per key, that's a real contract change from rc.0 and worth a line in the migration notes — happy to close and switch our projection to per-key tracking.
Gap? If deep() is meant to track the composed view (which is what rc.0 did and what the deep()/snapshot readers and per-key readers disagree while a transaction holds store landings #3147 test's "reader families agree" ruling reads like), then base-family writes are not bumping the view's deep witness. I looked at deep-held-visibility.test.ts and optimistic-observer-invariance.test.ts and both drive changes through the optimistic setter / reconcile, so a plain base write to a derived view doesn't seem to be pinned by a test yet.
Impact
In @dschz/solid-ag-grid the rowStore adapter binds one createEffect(() => deep(row), …) per row on the user's store (plain or optimistic view) and projects the returned plain payload into AG Grid transactions. On rc.1+ every optimistic-view user goes silent: optimistic writes still project, but the server-confirmed truth (and any later authoritative update) never reaches the grid. Plain stores are unaffected. This is currently the one thing holding our rc.0 → rc.7 bump.
Possibly related (happy to open separately)
While an optimistic overlay is active, view.map(r => r) on rc.7 returns fresh proxies for every row, not just the touched ones — identities are restored at revert/settle. rc.0 kept untouched rows identity-stable through the overlay. If that's intentional it's fine, just noting it since anything doing pointer-diffs over the view (we do, mapArray-style) now pays O(n) per optimistic action instead of O(delta).
Platform
OS: macOS 15 (Darwin 24.6.0)
Node 24.13.0, solid-js@2.0.0-rc.7 / @solidjs/signals@2.0.0-rc.7 (also reproduces in vitest/jsdom)
Summary
On
solid-js@2.0.0-rc.7(browser build), an effect that tracks acreateOptimisticStoreview throughdeep()never re-runs when the base store it derives from is written. A per-key read on the same view row re-runs fine, and reading the value afterwards shows the new data — so the write lands and the view serves it, butdeep()never hears about it.This holds for both shapes —
deep(view[0])on a row proxy anddeep(view)on the root — and needs no action, no optimistic write, and no in-flight transaction. Just a plain base write with the optimistic view sitting there.Works on rc.0. Broken from rc.1 onward (verified with
@solidjs/signalspinned to rc.1), which lines up with the store rewrite landing in rc.1 (0797215, "deep()subscribes one witness node per record").Minimal reproduction
Results (Node 24.13, macOS)
deep(view[0])deep(view)view[0].qtyview[0].qtyreads as22@solidjs/signalspinned to rc.1)22The same
deep(row)effect on a plaincreateStorerow (no optimistic view) re-runs on rc.7 as expected, so this is specific to the optimistic-view composition.Swapping
setBasefor an optimisticsetViewwrite does wakedeep(view[0])on rc.7 (it fires for the write and again for the revert). So the deep witness sees writes that go through the view's own family, but not writes that arrive from the base family it composes over.Where it looks like it goes wrong (from the rc.7
@solidjs/signalssource,src/store/next/store.ts)deep()→deepNext()walks targets directly and subscribes two nodes per record:getKeySetNode(t)and the lazy deep-witnessgetDeepNode(t)(target.dk). The child keys come fromReflect.ownKeys(readSource(t)), so no per-key nodes get linked on the way down.bumpDeep(t)— in the setter's notify (written-keys diff),notifyFold, and the reconcile walk. Those bumps land on the target that was written: the base family's target.t.ch, §7b). Per-key reads survive this becauseserveDataKeylinks through the chain. The$TRACKget-trap path also handles it explicitly: "a chained backing's$TRACKreads through to the INNER store's key-set" (2.0.0-beta-16 | optimistic update is not being rolledback when rendered from a derived store and a<For>#2864 / core R21,readSource(target)[$TRACK]).deepNext'swalkThas no equivalent read-through: it reads the view target'skanddkonly. Nothing that happens on the base family ever touches those two nodes, so a base write is invisible to adeep()subscriber on the view — while an optimistic write (which goes through the view family's own setter) bumps them and fires.The
$TRACKread-through rule looks like the closest precedent:walkThas no equivalent for the inner target's key-set and deep-witness nodes whent.chis set.The question
deep()over an optimistic view is meant to be a view-family-only subscription and base changes are expected to be tracked per key, that's a real contract change from rc.0 and worth a line in the migration notes — happy to close and switch our projection to per-key tracking.deep()is meant to track the composed view (which is what rc.0 did and what the deep()/snapshot readers and per-key readers disagree while a transaction holds store landings #3147 test's "reader families agree" ruling reads like), then base-family writes are not bumping the view's deep witness. I looked atdeep-held-visibility.test.tsandoptimistic-observer-invariance.test.tsand both drive changes through the optimistic setter / reconcile, so a plain base write to a derived view doesn't seem to be pinned by a test yet.Impact
In
@dschz/solid-ag-gridtherowStoreadapter binds onecreateEffect(() => deep(row), …)per row on the user's store (plain or optimistic view) and projects the returned plain payload into AG Grid transactions. On rc.1+ every optimistic-view user goes silent: optimistic writes still project, but the server-confirmed truth (and any later authoritative update) never reaches the grid. Plain stores are unaffected. This is currently the one thing holding our rc.0 → rc.7 bump.Possibly related (happy to open separately)
While an optimistic overlay is active,
view.map(r => r)on rc.7 returns fresh proxies for every row, not just the touched ones — identities are restored at revert/settle. rc.0 kept untouched rows identity-stable through the overlay. If that's intentional it's fine, just noting it since anything doing pointer-diffs over the view (we do,mapArray-style) now pays O(n) per optimistic action instead of O(delta).Platform
solid-js@2.0.0-rc.7/@solidjs/signals@2.0.0-rc.7(also reproduces in vitest/jsdom)