Skip to content

2.0.0-beta.15: snapshot() / deep() ignore optimistic writes on createOptimisticStore #2850

Description

@brenelz

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)
  • reconcilesrc/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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions