Repository navigation
Description
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