Skip to content

2.0.0-beta.19: Optimistic lanes: a shared subscriber merges lanes across transactions #2912

Description

@brenelz

Version: 2.0.0-beta.19, branch next (reproduced @ c7bb2c8, post-#2899; the mechanism is untouched by #2902#2904)

Summary

When one effect subscribes to keys/signals touched by two different actions, insertSubsassignOrMergeLane merges the earlier action's lane into the later action's lane without merging their transactions. From then on resolveTransition reports the later action's transaction for the earlier action's nodes. Two consequences:

Notably, the shipped #2899 test 3 passes only because of write order: mirror it with action B's two writes swapped (disjoint key first, same key second) and it fails.

Root cause

packages/solid-signals/src/core/lanes.ts:159mergeLanes(sourceRoot, existingRoot) inside assignOrMergeLane, reached from insertSubs (core/scheduler.ts:621), merges lanes from different transactions. resolveTransition (lanes.ts:118-123) then prefers the hijacked lane's _transition. Consumers: core/optimistic.ts:85, the store owner stamp (store/store.ts:621-624), and the scoped clear (store/optimistic.ts:155-193).

Repros

The shipped #2899 test with B's writes swapped

const [s, setS] = createOptimisticStore({ a: 1, b: 2 });
createRoot(() => createRenderEffect(() => s.a + s.b, () => {})); // ONE effect reads both
flush();

const pA = action(function* () { setS(d => { d.a = 10; }); yield gA.promise; })();
flush();
const pB = action(function* () {
  setS(d => { d.b = 20; });  // disjoint key FIRST — hijacks A's lane via the shared effect
  setS(d => { d.a = 11; });  // same key as A — should merge the transactions
  yield gB.promise;
})();
flush();
// [s.a, s.b] === [11, 20] ✓

gB.resolve(); await pB; flush();
[s.a, s.b]; // FAILS: [1, 2] — both keys reverted while A is still in flight.
            // Expected [11, 20]: merged same-key semantics hold until the LAST action.

Plain createOptimistic signals, three actions

One effect reads x() + y(). Action A writes x, action B writes y (hijacks x's lane into B's transaction), action C rewrites x — C entangles with B instead of A. When A settles, x reverts to committed while C is still in flight (observed 1, expected 11). A two-store variant fails the same way.

Impact

HIGH — one component reading two independently-mutated values is the normal case of optimistic UI, and overlapping actions silently revert each other's live overrides. The fix likely needs a design decision at the lane/transaction ownership boundary: either merge transactions when lanes merge, or key resolveTransition off the node's own transaction rather than the lane's.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions