Skip to content

Atom: an Effect atom's first failure drops initialValue from previousSuccess, though a Stream atom keeps it #9605

Closed

Description

@nick-inkeep

What version of Effect is running?

[email protected], Node 26.11.1. The code is unchanged on main (bbedfc71c0).

What steps can reproduce the bug?

Atom.make(effect, { initialValue }) shows Success(initialValue, waiting: true) while the first run is pending. If that run fails, the Failure has previousSuccess: None, so AsyncResult.value and getOrElse lose the value the atom was just showing. A Stream atom with the same option, and the same value seeded through registry initialValues, keep it.

import { Effect, Option, Stream } from "effect"
import * as AsyncResult from "effect/reactivity/AsyncResult"
import * as Atom from "effect/reactivity/Atom"
import * as AtomRegistry from "effect/reactivity/AtomRegistry"

const failLater = Effect.sleep(10).pipe(Effect.andThen(Effect.fail("boom")))

const run = async (label, atom, initialValues) => {
  const registry = AtomRegistry.make({ initialValues })
  const unmount = registry.mount(atom)
  const before = registry.get(atom)
  await new Promise((r) => setTimeout(r, 50))
  const after = registry.get(atom)
  const prev = Option.map(after.previousSuccess, (s) => s.value)
  console.log(label, `${before._tag}(${before.value})`, "->", after._tag, "previousSuccess:", Option.getOrElse(Option.map(prev, (v) => `Some(${v})`), () => "None"))
  unmount()
}

await run("effect + initialValue: ", Atom.make(failLater, { initialValue: "cached" }))
await run("stream + initialValue: ", Atom.make(Stream.fromEffect(failLater), { initialValue: "cached" }))
const seeded = Atom.make(failLater)
await run("effect + registry seed:", seeded, [Atom.initialValue(seeded, AsyncResult.success("cached"))])
effect + initialValue:  Success(cached) -> Failure previousSuccess: None
stream + initialValue:  Success(cached) -> Failure previousSuccess: Some(cached)
effect + registry seed: Success(cached) -> Failure previousSuccess: Some(cached)

What is the expected behavior?

When the first run of an Effect atom created with initialValue fails after the atom has shown the initial value, the Failure carries that value as previousSuccess, as a Stream atom with the same option does, and as Failure documents ("the latest previous success when one is available").

What do you see instead?

previousSuccess is None. The same applies to the function form (Atom.make((get) => effect, { initialValue })) and to runtime.atom(effect, { initialValue }). It also drops a Success the effect itself published with get.setSelf before failing.

Additional information

makeEffect reads previous = ctx.self() once, before the effect runs (Atom.ts#L564). On the first build that is None, and the exit callback builds the result from it (#L579) even though the atom has since exposed waiting(success(initialValue)) (#L551-L553, #L595). makeStream reads ctx.self() when the stream fails (#L923-L926), the change tim-smart/effect-atom#341 made for tim-smart/effect-atom#342; makeEffect kept the snapshot. On main the same lines are Atom.ts L564 and L583.

A synchronous failure (Effect.fail(...) with no async boundary) returns previousSuccess: None for both Effect and Stream atoms; there the atom never showed the initial value.

A failing test in the style of Atom.test.ts, placed after "stream failure keeps previousSuccess", fails on main:

it("effect failure keeps initialValue as previousSuccess", async () => {
  const atom = Atom.make(Effect.fail("boom").pipe(Effect.delay(100)), { initialValue: 0 })
  const r = AtomRegistry.make()
  const cancel = r.mount(atom)

  const before = r.get(atom)
  assert(AsyncResult.isSuccess(before))
  assert.strictEqual(before.value, 0)

  await vitest.advanceTimersByTimeAsync(100)
  const afterFail = r.get(atom)
  assert(AsyncResult.isFailure(afterFail))
  const prev = AsyncResult.value(afterFail)
  assert(Option.isSome(prev))
  assert.strictEqual(prev.value, 0)

  cancel()
})

The same test with Stream.fromEffect(...) in place of the effect passes.

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