Skip to content

Commit 89cd267

Browse files
authored
Fix Atom dependencies during batch rebuilds (#423)
1 parent 5a486fb commit 89cd267

3 files changed

Lines changed: 77 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect-atom/atom": patch
3+
---
4+
5+
Fix Atom dependency tracking and re-entrant invalidation during batch rebuilds.

packages/atom/src/internal/registry.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ class Node<A> {
289289
children = new Set<Node<any>>()
290290
listeners = new Set<() => void>()
291291
skipInvalidation = false
292+
building = false
293+
invalidatedDuringBuild = false
292294

293295
get canBeRemoved(): boolean {
294296
return !this.atom.keepAlive && this.listeners.size === 0 && this.children.size === 0 &&
@@ -299,7 +301,9 @@ class Node<A> {
299301
value(): A {
300302
if ((this.state & NodeFlags.waitingForValue) !== 0) {
301303
this.lifetime = makeLifetime(this)
304+
this.building = true
302305
const value = this.atom.read(this.lifetime)
306+
this.building = false
303307
if ((this.state & NodeFlags.waitingForValue) !== 0) {
304308
this.setValue(value)
305309
}
@@ -383,6 +387,9 @@ class Node<A> {
383387
}
384388

385389
invalidate(): void {
390+
if (this.building && batchState.phase === BatchPhase.collect) {
391+
this.invalidatedDuringBuild = true
392+
}
386393
if (this.state === NodeState.valid) {
387394
this.state = NodeState.stale
388395
this.disposeLifetime()
@@ -512,8 +519,9 @@ const LifetimeProto: Omit<Lifetime<any>, "node" | "finalizers" | "disposed" | "i
512519
throw disposedError(this.node.atom)
513520
}
514521
const parent = this.node.registry.ensureNode(atom)
522+
const value = parent.value()
515523
this.node.addParent(parent)
516-
return parent.value()
524+
return value
517525
},
518526

519527
result<A, E>(this: Lifetime<any>, atom: Atom.Atom<Result.Result<A, E>>, options?: {
@@ -795,7 +803,12 @@ export function batch(f: () => void): void {
795803

796804
function batchRebuildNode(node: Node<any>) {
797805
if (node.state === NodeState.valid) {
798-
return
806+
if (!node.invalidatedDuringBuild) {
807+
return
808+
}
809+
node.invalidatedDuringBuild = false
810+
node.state = NodeState.stale
811+
node.disposeLifetime()
799812
}
800813

801814
for (const parent of node.parents) {

packages/atom/test/Atom.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,63 @@ describe("Atom", () => {
697697
expect(r.get(derived)).toEqual("2b")
698698
})
699699

700+
it("retains method-form dependencies added during a batch rebuild", async () => {
701+
const registry = Registry.make()
702+
const source = Atom.make(Option.none<string>())
703+
const gate = Effect.unsafeMakeLatch()
704+
const asyncAtom = Atom.make((get) =>
705+
Effect.gen(function*() {
706+
const value = get(source)
707+
if (Option.isNone(value)) {
708+
return yield* Effect.fail("SourceIsNone" as const)
709+
}
710+
yield* gate.await
711+
return `computed-${value.value}`
712+
})
713+
)
714+
const derived = Atom.make((get): unknown => {
715+
const value = get.get(source)
716+
if (Option.isNone(value)) {
717+
return "empty"
718+
}
719+
return get.get(asyncAtom)
720+
})
721+
722+
registry.subscribe(derived, () => {}, { immediate: true })
723+
registry.subscribe(asyncAtom, () => {}, { immediate: true })
724+
725+
Atom.batch(() => registry.set(source, Option.some("a")))
726+
727+
gate.unsafeOpen()
728+
await Effect.runPromise(Effect.yieldNow())
729+
730+
const result = registry.get(derived) as Result.Result<string, "SourceIsNone">
731+
assert(Result.isSuccess(result))
732+
assert.strictEqual(result.value, "computed-a")
733+
})
734+
735+
it("rebuilds an atom invalidated during its own batch rebuild", () => {
736+
const registry = Registry.make()
737+
const source = Atom.make(0)
738+
const enabled = Atom.make(false)
739+
const updateSource = Atom.make((get) => {
740+
get.set(source, 1)
741+
})
742+
const derived = Atom.make((get) => {
743+
const value = get(source)
744+
if (get(enabled)) {
745+
get(updateSource)
746+
}
747+
return value
748+
})
749+
750+
registry.subscribe(derived, () => {}, { immediate: true })
751+
752+
Atom.batch(() => registry.set(enabled, true))
753+
754+
assert.strictEqual(registry.get(derived), 1)
755+
})
756+
700757
it("nested batch", async () => {
701758
const r = Registry.make()
702759
const state = Atom.make(1).pipe(Atom.keepAlive)

0 commit comments

Comments
 (0)