Skip to content

createOptimisticStore overlay is not reverted after a failed action while attribution is enabled (rc.10 regression) #3687

Description

@ethan-huo

When the attribution engine is on — captureArtifact from @solidjs/diagnostics, or the performance tracks @solidjs/vite-plugin 3.0.0-next.46 enables by default under vite dev — an optimistic store write made inside an action that fails is not reverted when the action settles. The same code reverts correctly with attribution off, and reverts under attribution on rc.9.

createOptimistic (signal) is not affected. The source kind does not matter (Promise and AsyncIterable both reproduce).

Steps (full test below):

  1. createOptimisticStore(() => source, [], { key: 'id' }) with one row { id: 'a', done: false }.
  2. An action(function* () { setList(l => { l[0].done = true }); try { yield Promise.reject(new Error('x')) } catch {} }).
  3. Run it inside captureArtifact(...), flush, and read the row after settle.

Expected: done === false (the overlay drops at settle; RFC 06).
Actual (rc.10, attribution on): done === true.

Results of the test below:

Case rc.9 rc.10
store, Promise source, attribution off pass pass
store, Promise source, captureArtifact pass fail (a:true)
store, AsyncIterable source, attribution off pass pass
store, AsyncIterable source, captureArtifact pass fail (a:true)
createOptimistic(false), attribution off pass pass
createOptimistic(false), captureArtifact pass pass

Related symptoms seen in the same scenario family (larger test, not minimized):

  • an OPTIMISTIC_REVERTED finding reporting "showed 1; it settled to 1" (same value) under expectNoDiagnostics;
  • an unhandled rejection TypeError: Cannot read properties of undefined (reading '_host') during the run.

Observation changing runtime semantics also means dev servers with the default performance tracks behave differently from production for optimistic stores.

Environment:

  • solid-js, @solidjs/web, @solidjs/signals, @solidjs/diagnostics: 2.0.0-rc.10 (compared with 2.0.0-rc.9)
  • @solidjs/vite-plugin 3.0.0-next.46 (rc.9 comparison: 3.0.0-next.44)
  • vitest via vite-plus 0.3.3, jsdom; Node v26.10.0; Bun 1.4.2; macOS arm64

Reproduction

import { captureArtifact } from '@solidjs/diagnostics'
import { action, createOptimistic, createOptimisticStore, createRoot, createEffect, flush } from 'solid-js'
import { expect, it } from 'vitest'

const tick = () => new Promise((r) => setTimeout(r, 0))
async function settle() { for (let i = 0; i < 4; i++) { await tick(); flush() } }

type Row = { id: string; done: boolean }
function liveSource(initial: Row[]): AsyncIterable<Row[]> {
  return { async *[Symbol.asyncIterator]() { yield initial; await new Promise(() => {}) } }
}

for (const source of ['promise', 'async-iterable'] as const)
  for (const observed of [false, true]) {
    it(`failed action reverts: source=${source} captureArtifact=${observed}`, async () => {
      const body = async () => {
        const seen: string[] = []
        let run!: () => Promise<unknown>
        const dispose = createRoot((d) => {
          const init = [{ id: 'a', done: false }]
          const [list, setList] = createOptimisticStore<Row[]>(() => source === 'promise' ? Promise.resolve(init) : liveSource(init), [], { key: 'id' })
          createEffect(() => list.map((r) => `${r.id}:${r.done}`).join(' '), (v) => { seen.push(v) })
          const act = action(function* () {
            setList((l) => { if (l[0]) l[0].done = true })
            try { yield Promise.reject(new Error('x')) } catch {}
          })
          run = () => act()
          return d
        })
        await settle(); await run(); await settle()
        dispose()
        return seen.at(-1)
      }
      let last: string | undefined
      if (observed) await captureArtifact(async () => { last = await body() }, { scenario: 'probe' })
      else last = await body()
      expect(last).toBe('a:false')
    })
  }

for (const observed of [false, true])
  it(`createOptimistic(false) reverts after caught failure: captureArtifact=${observed}`, async () => {
    const body = async () => {
      let read!: () => boolean, run!: () => Promise<unknown>
      const dispose = createRoot((d) => {
        const [busy, setBusy] = createOptimistic(false)
        read = busy
        createEffect(() => busy(), () => {})
        const act = action(function* () { setBusy(true); try { yield Promise.reject(new Error('x')) } catch {} })
        run = () => act()
        return d
      })
      await settle(); await run(); await settle()
      const v = read(); dispose(); return v
    }
    let last: boolean | undefined
    if (observed) await captureArtifact(async () => { last = await body() }, { scenario: 'probe2' })
    else last = await body()
    expect(last).toBe(false)
  })

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