Skip to content

Commit abce499

Browse files
authored
fix(atom): prevent listeners from being skipped during notify (#404)
1 parent 7e64d3f commit abce499

3 files changed

Lines changed: 41 additions & 17 deletions

File tree

.changeset/whole-spoons-fall.md

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+
use Set for node listeners to prevent skipping during notify

packages/atom/src/internal/registry.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import * as Result from "../Result.js"
1212

1313
const constImmediate = { immediate: true }
1414

15+
const notifyListener = (listener: () => void): void => {
16+
listener()
17+
}
18+
1519
type TypeId = "~effect-atom/atom/Registry"
1620
const TypeId: TypeId = "~effect-atom/atom/Registry" as const
1721

@@ -287,11 +291,11 @@ class Node<A> {
287291
parents: Array<Node<any>> = []
288292
previousParents: Array<Node<any>> | undefined
289293
children: Array<Node<any>> = []
290-
listeners: Array<() => void> = []
294+
listeners: Set<() => void> = new Set()
291295
skipInvalidation = false
292296

293297
get canBeRemoved(): boolean {
294-
return !this.atom.keepAlive && this.listeners.length === 0 && this.children.length === 0 &&
298+
return !this.atom.keepAlive && this.listeners.size === 0 && this.children.length === 0 &&
295299
this.state !== 0
296300
}
297301

@@ -352,7 +356,7 @@ class Node<A> {
352356
this.invalidateChildren()
353357
}
354358

355-
if (this.listeners.length > 0) {
359+
if (this.listeners.size > 0) {
356360
if (batchState.phase === BatchPhase.collect) {
357361
batchState.notify.add(this)
358362
} else {
@@ -397,7 +401,7 @@ class Node<A> {
397401

398402
if (batchState.phase === BatchPhase.collect) {
399403
batchState.stale.push(this)
400-
} else if (this.atom.lazy && this.listeners.length === 0 && !childrenAreActive(this.children)) {
404+
} else if (this.atom.lazy && this.listeners.size === 0 && !childrenAreActive(this.children)) {
401405
this.invalidateChildren()
402406
this.skipInvalidation = true
403407
} else {
@@ -418,9 +422,7 @@ class Node<A> {
418422
}
419423

420424
notify(): void {
421-
for (let i = 0; i < this.listeners.length; i++) {
422-
this.listeners[i]()
423-
}
425+
this.listeners.forEach(notifyListener)
424426

425427
if (batchState.phase === BatchPhase.commit) {
426428
batchState.notify.delete(this)
@@ -441,7 +443,7 @@ class Node<A> {
441443

442444
remove() {
443445
this.state = NodeState.removed
444-
this.listeners = []
446+
this.listeners.clear()
445447

446448
if (this.lifetime === undefined) {
447449
return
@@ -464,14 +466,8 @@ class Node<A> {
464466
}
465467

466468
subscribe(listener: () => void): () => void {
467-
this.listeners.push(listener)
468-
return () => {
469-
const index = this.listeners.indexOf(listener)
470-
if (index !== -1) {
471-
this.listeners[index] = this.listeners[this.listeners.length - 1]
472-
this.listeners.pop()
473-
}
474-
}
469+
this.listeners.add(listener)
470+
return () => this.listeners.delete(listener)
475471
}
476472
}
477473

@@ -485,7 +481,7 @@ function childrenAreActive(children: Array<Node<any>>): boolean {
485481
while (current !== undefined) {
486482
for (let i = 0, len = current.length; i < len; i++) {
487483
const child = current[i]
488-
if (!child.atom.lazy || child.listeners.length > 0) {
484+
if (!child.atom.lazy || child.listeners.size > 0) {
489485
return true
490486
} else if (child.children.length > 0) {
491487
if (stack === undefined) {

packages/atom/test/Atom.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,29 @@ describe("Atom", () => {
7070
expect(r.get(counter)).toEqual(0)
7171
})
7272

73+
it("subscribe does not skip listeners when unsubscribing during notify", () => {
74+
const counter = Atom.make(0)
75+
const r = Registry.make()
76+
let first = 0
77+
let second = 0
78+
let cancelFirst = () => {
79+
}
80+
81+
cancelFirst = r.subscribe(counter, () => {
82+
first++
83+
cancelFirst()
84+
})
85+
86+
r.subscribe(counter, () => {
87+
second++
88+
})
89+
90+
r.set(counter, 1)
91+
92+
expect(first).toEqual(1)
93+
expect(second).toEqual(1)
94+
})
95+
7396
it("runtime", async () => {
7497
const count = counterRuntime.atom(Effect.flatMap(Counter, (_) => _.get))
7598
const r = Registry.make()

0 commit comments

Comments
 (0)