Skip to content

Optimistic store property readers stay stale after reconciliation removes a property #3883

Closed

Description

@GabbeV

Describe the bug

When an action adds an optimistic row and then retains it in a writable derived store with createFailed: true, a later source update reconciles the row without that property. Both the derived store and its optimistic view then contain { id: "A" }, but JSX reading cards[0]?.createFailed continues displaying Error.

The action has already settled before the source update. There are no pending actions when the stale UI remains.

Your Example Website or App

https://s.olid.uk/id/iKS1wleST9GNN_nKlC2RDg

Steps to Reproduce the Bug or Issue

  1. Open the reproduction and click Run once.
  2. The action optimistically inserts a row, then retains it with createFailed: true after 100 ms.
  3. After that action settles and another 200 ms passes, the source supplies { id: "A" }.
  4. The page remains on Error, although reconciliation has removed createFailed from both stores.

Expected behavior

The page should return to Confirmed when reconciliation removes createFailed, since reading the property now returns undefined.

Screenshots or Videos

No response

Platform

  • OS: Linux (WSL)
  • Browser: Chromium 153
  • Solid: Zulu preview bc51ada (solid-js, @solidjs/signals, @solidjs/web)

Additional context

Controls tested against the same build:

  • Removing createOptimisticStore and rendering the derived store directly updates to Confirmed.
  • Supplying { id: "A", createFailed: false } instead also updates correctly.
  • The failure reproduces without For, routing, or a database.

Direct snapshots after the source update show source, local, and cards all equal to [{ "id": "A" }], while the property-dependent JSX still displays Error.

Activity

  1. GabbeV commented on Oct 7, 2026

    @GabbeV
    Author

    Narrowed this to the L2 rewrite in #3774:

    • Published RC.13 renders the reproduction correctly.
    • A core-only equivalent passes at 41fdf9696 (the merge's parent) and fails at 1b9ceb679 (the merge itself).
    • It still fails at bc51ada.

    The suspected defect is the absent-own-key branch of the store get trap. Present keys go through serveDataKey, which reads through a chained backing unless the outer node carries an optimistic override. The absent-key branch instead returns nodeValue directly, leaving the outer node's cached true visible after the inner store removes the property.

    A clarification to the original evidence: JSON snapshots omit the key, but a direct property read can still return the stale true. The core observer also re-runs and reads true, so this is more specific than merely a missed notification.

    As an isolated control, inserting this immediately before that absent-key branch makes the core reproduction pass:

    if (target.ch && src === target.v)
      return serveDataKey(target, key, v, src, node0, accProbe);

    That is a diagnostic control, not a full-suite-validated patch.

  2. ryansolid commented on Oct 8, 2026

    @ryansolid
    Member

    Thanks @GabbeV — the bisect to #3774 and your two-line fix were spot on. It landed as-is with tests in #3885 and will ship in rc.14.

    Root cause: in an optimistic store over another store, the outer store's per-property node is only a subscription point; the inner store holds the real value unless the outer node carries an optimistic guess. Present keys read through to the inner store, but a key the outer object doesn't have returned the outer node's cached value, so a property reconciliation removed kept showing its last value.

    — Claude via Cursor

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