Summary
Every read path of an optimistic store — direct proxy reads, memos, effects, render
effects — sees optimistic writes immediately. snapshot() (and deep(), which shares
the same implementation) does not: it silently returns the last committed value.
For a regular store, snapshot() is explicitly designed to agree with reactive
readers — it reads the pending-write overlay synchronously. For an optimistic
store the overlay simply lives under a different key, and snapshot was never taught
about it. The result is that serializing a store (snapshot's documented use case:
"logging, serialization, structured-clone, network payloads") disagrees with what the
UI is showing, with no error or warning.
Reproduction
Minimal (fails on current next):
import { createOptimisticStore, snapshot } from "@solidjs/signals";
const [state, setState] = createOptimisticStore({ name: "John" });
setState(s => {
s.name = "Jake";
});
state.name; // "Jake" — every reactive/proxy read sees the optimistic value
snapshot(state).name; // "John" — BUG: snapshot silently returns the committed value
Root cause
A StoreNode has three value layers (src/store/store.ts:77–93):
| Key |
Field |
Contents |
"v" |
STORE_VALUE |
raw committed target |
"o" |
STORE_OVERRIDE |
pending-write overlay (regular stores) |
"x" |
STORE_OPTIMISTIC_OVERRIDE |
optimistic overlay (optimistic stores) |
For a store created by createOptimisticStore, all user writes are routed into
STORE_OPTIMISTIC_OVERRIDE (prepareStoreWrite, src/store/store.ts:346–349), never
into STORE_VALUE/STORE_OVERRIDE. The overlay is discarded (reverted) when the
owning transition settles (clearOptimisticOverride, src/store/optimistic.ts:111–167).
Every other consumer of store values was updated to consult the optimistic overlay
first:
get trap — src/store/store.ts:448–458 ("Check optimistic override first, then
regular override, then base value")
has (537–543), set prev-value (569–580), deleteProperty (683–704),
ownKeys (713–723), getOwnPropertyDescriptor (728–746)
reconcile — src/store/reconcile.ts:99, :248
snapshotImpl is the one consumer that was not:
// src/store/utils.ts:34
const override = target[STORE_OVERRIDE]; // STORE_OPTIMISTIC_OVERRIDE never consulted
Summary
Every read path of an optimistic store — direct proxy reads, memos, effects, render
effects — sees optimistic writes immediately.
snapshot()(anddeep(), which sharesthe same implementation) does not: it silently returns the last committed value.
For a regular store,
snapshot()is explicitly designed to agree with reactivereaders — it reads the pending-write overlay synchronously. For an optimistic
store the overlay simply lives under a different key, and
snapshotwas never taughtabout it. The result is that serializing a store (
snapshot's documented use case:"logging, serialization, structured-clone, network payloads") disagrees with what the
UI is showing, with no error or warning.
Reproduction
Minimal (fails on current
next):Root cause
A
StoreNodehas three value layers (src/store/store.ts:77–93):"v"STORE_VALUE"o"STORE_OVERRIDE"x"STORE_OPTIMISTIC_OVERRIDEFor a store created by
createOptimisticStore, all user writes are routed intoSTORE_OPTIMISTIC_OVERRIDE(prepareStoreWrite,src/store/store.ts:346–349), neverinto
STORE_VALUE/STORE_OVERRIDE. The overlay is discarded (reverted) when theowning transition settles (
clearOptimisticOverride,src/store/optimistic.ts:111–167).Every other consumer of store values was updated to consult the optimistic overlay
first:
gettrap —src/store/store.ts:448–458("Check optimistic override first, thenregular override, then base value")
has(537–543),setprev-value (569–580),deleteProperty(683–704),ownKeys(713–723),getOwnPropertyDescriptor(728–746)reconcile—src/store/reconcile.ts:99,:248snapshotImplis the one consumer that was not: