Version: 2.0.0-beta.19 (branch next)
Summary
With two overlapping actions writing disjoint properties of the same createOptimisticStore, the first action to settle wipes the other action's still-live optimistic overrides. The store's override layer (STORE_OPTIMISTIC_OVERRIDE) is one flat layer per store, and settle clears it wholesale rather than per-transition.
The signal-side equivalent already guarantees the opposite: the existing spec test "holds same-value optimistic writes until all overlapping actions settle" passes for createOptimistic. The store side violates that guarantee.
Root cause
- Store→transition ownership is tracked as a flat per-transition set:
trackOptimisticStore (packages/solid-signals/src/core/optimistic.ts:310).
- When any owning transition settles,
clearOptimisticStores → clearOptimisticOverride deletes the entire layer: delete target[STORE_OPTIMISTIC_OVERRIDE] (packages/solid-signals/src/store/optimistic.ts:130), reverting every overridden node regardless of which transition wrote it.
- The two actions never entangle into one transition, because entanglement requires a shared node write (via
optimisticWrite's initTransition) or a $TRACK node — prepareStoreWrite (store/store.ts:556) only entangles via firewall._transition, which plain-form optimistic stores don't have. Disjoint-property writes therefore keep two independent transitions sharing one override layer.
Repro
const [s, setS] = createOptimisticStore({ a: 1, b: 2 });
createRoot(() => {
createRenderEffect(() => s.a, () => {});
createRenderEffect(() => s.b, () => {});
});
flush();
const actA = action(function* () {
setS(d => { d.a = 10; });
yield gateA.promise;
});
const actB = action(function* () {
setS(d => { d.b = 20; });
yield gateB.promise;
});
const pA = actA();
flush();
// s.a === 10 ✓
const pB = actB();
flush();
// s.b === 20 ✓, s.a === 10 ✓
gateB.resolve(); // settle B first
await pB;
flush();
s.a; // expected: 10 (action A still in flight) — actual: 1 (override wiped)
gateA.resolve();
await pA;
Existing optimistic/transition spec suites pass on the same checkout.
Impact
HIGH — any UI with two concurrent optimistic mutations touching the same store (e.g. two in-flight form submissions updating different rows) visibly reverts the still-pending mutation the moment the other one finishes, then has no way to recover until its own settle.
Version: 2.0.0-beta.19 (branch
next)Summary
With two overlapping actions writing disjoint properties of the same
createOptimisticStore, the first action to settle wipes the other action's still-live optimistic overrides. The store's override layer (STORE_OPTIMISTIC_OVERRIDE) is one flat layer per store, and settle clears it wholesale rather than per-transition.The signal-side equivalent already guarantees the opposite: the existing spec test "holds same-value optimistic writes until all overlapping actions settle" passes for
createOptimistic. The store side violates that guarantee.Root cause
trackOptimisticStore(packages/solid-signals/src/core/optimistic.ts:310).clearOptimisticStores→clearOptimisticOverridedeletes the entire layer:delete target[STORE_OPTIMISTIC_OVERRIDE](packages/solid-signals/src/store/optimistic.ts:130), reverting every overridden node regardless of which transition wrote it.optimisticWrite'sinitTransition) or a$TRACKnode —prepareStoreWrite(store/store.ts:556) only entangles viafirewall._transition, which plain-form optimistic stores don't have. Disjoint-property writes therefore keep two independent transitions sharing one override layer.Repro
Existing optimistic/transition spec suites pass on the same checkout.
Impact
HIGH — any UI with two concurrent optimistic mutations touching the same store (e.g. two in-flight form submissions updating different rows) visibly reverts the still-pending mutation the moment the other one finishes, then has no way to recover until its own settle.