Skip to content

2.0.0-rc until doesnt entangle the update resolving it with the action update #3164

Closed

Description

@GabbeV

Describe the bug

This is an example demonstrating why the fix for #3123 (comment) is insufficient. Not entangling when until is set makes the granularity of the store decide what tears or not leading to weird issues where object might break type contracts without type errors.

Your Example Website or App

https://stackblitz.com/edit/solidjs-templates-dkufnzkf?file=src%2FApp.tsx

Steps to Reproduce the Bug or Issue

  1. Open repro
  2. Click button
  3. Observe that the seemingly correct code errors part way through the update.

Expected behavior

The authentication entangle with the action making avoiding the tearing causing the error.

Screenshots or Videos

No response

Platform

  • OS: Windows
  • Browser: Firefox

Additional context

No response

Activity

  1. changed the title [-]2.0.0-rc until doesnt entangle the two update resolving it with the action update[/-] [+]2.0.0-rc until doesnt entangle the update resolving it with the action update[/+] on Sep 1, 2026
  2. ryansolid commented on Sep 1, 2026

    @ryansolid
    Member

    Fixed on next in a536e29 — and your instinct in the #3123 thread was right: we re-ruled this to entangle.

    Truth landings under live optimism now fold: while any transaction retains optimism on the family (or an armed signal), fresh authoritative data — continuation yields, foreign refetches, replacing landings alike — stages into the retaining transaction and reveals atomically when it settles, the same semantics as a signal landing under an active override. Ordinary readers keep the committed view until that reveal, so no frame can compose override + landed truth (the union tear from your repro), and the authoritative version bump and the optimism's removal now land together.

    until() and latest() tunnel through the hold: authoritative reads see the staged truth, and a landing staged under an active override wakes waiting until() predicates — that was the deadlock in your signal-path variant. This also retired the retained-setter replay machinery from the interim #3123 fix; the fold subsumes it.

    Both repros from the report are pinned as regression tests (store union-tear + atomic reveal, signal until() wake). Will ship in rc.5.

  3. GabbeV commented on Sep 1, 2026

    @GabbeV
    Author

    @ryansolid this repro also kind of demonstrates why I'm a bit worried about entanglement semantics. They decide where the boundaries of consistency are. Choosing to make entanglement too fine grained in stores might cause issues like this where the intent of the user was to entangle with things they didn't actually touch. In this example we only touched status but intended to hold the entire object. Using equality to determine entanglement could have the same effect where the user writes something they assume should entangle but it happens to be equal and therefore doesn't. Cross primitives this is basically an unavoidable problem to some degree as we can't predict what signals you didn't set but within a store you might even break typescript types as demonstrated here if you let entanglement be based on sub store granularity.

  4. GabbeV commented on Sep 1, 2026

    @GabbeV
    Author

    Having AI explain the change that landed here to me also revealed that we still don't entangle based on until.

    I had AI write up the reasoning:

    I still think until() itself needs to entangle with the update that makes it truthy.

    The new fold rule only catches this when the optimism and confirmation touch the same store family. For example:

    const save = action(function* () {
      setSaving(true);
      yield mutate();
      yield until(() => stream.version >= expectedVersion);
    });

    If the stream lands version + new data, that can still reveal before saving reverts because the optimism lives on a different primitive.

    That gives:

    saving=true,  version=old, data=old
    saving=true,  version=new, data=new
    saving=false, version=new, data=new
    

    I think the middle state is the same tearing problem. until() is the explicit statement that this update confirms the action, so the update making it truthy should entangle with it.

  5. GabbeV commented on Sep 1, 2026

    @GabbeV
    Author

    To clarify the AIs example this will seemingly work until either unentangled update happens to be waiting for something more than this exact thing as if this is all that is going on then the timing will just make them reveal/revert at the same time anyway.

  6. ryansolid commented on Sep 1, 2026

    @ryansolid
    Member

    You were right, and this is now in for rc.5: until() entangles the update that flips it (1ece086f, refined in 1ac14eb6 on next).

    The semantics landed exactly on your framing — until() is the user's explicit declaration of what confirms the action, so we don't guess at relatedness:

    • When an awaited predicate settles truthy, whatever live transition (or ambient batch) staged the values it read IS the confirming event. Its staged cargo moves into the awaiting action's transaction and reveals at the joint settle. Your middle state — saving=true, version=new, data=new — can no longer paint: ordinary readers hold the pre-confirmation view until the action's optimism reverts and the confirmation commits, in one frame.
    • Falsy evaluations entangle nothing. Unrelated traffic on the watched sources (a non-flipping landing, another action's writes) reveals on its own schedule. The boundary of consistency is the flip itself, not the store/signal graph — which also answers your granularity worry for the cross-primitive case: the predicate names the condition, so entanglement is exactly as coarse or fine as what you wrote.
    • Within a store family, the fold from the previous fix already holds at family granularity (touching status holds the whole session object — your union repro), so sub-store granularity can't split a typed object into a shape no timeline contained.
    • The steal takes only the confirming transition's staged values, not its future: an open stream that echoes your write can flip the predicate without chaining the action to landings that haven't happened (a full merge deadlocked exactly that way — the iterable's reporter never completes). latest() and other authoritative reads still tunnel through the hold, and until() keeps its timeout/AbortSignal valves so a dropped confirmation can't hold a transaction forever.

    Your exact scenario (signal-side optimism + foreign store stream confirming) is pinned as a regression test, along with the non-flipping-traffic case, a foreign signal-transition case, and an already-truthy fast path. Thanks for pushing on this one — the report was correct that fold-only semantics made the store's granularity decide what tears.

  7. GabbeV commented on Sep 1, 2026

    @GabbeV
    Author

    I discussed it a bit more with AI and seems like the store family thing mostly solves the granularity but only for optimistic stores. Similar issues like this could happen with regular stores as well so might be reasonable to consider something similar for regular stores. But I understand there is a tension here given some user will use a mega store for their entire app. In the future maybe we could explore a api that allow you to express these boundaries when constructing the store somehow but given the current api there probably isn't a perfect solution.

  8. 5 remaining items

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