Skip to content

Commit 75f85cb

Browse files
committed
effects spawn w/ _watches=1 so they are alive by default
1 parent 8fb68e6 commit 75f85cb

2 files changed

Lines changed: 42 additions & 29 deletions

File tree

packages/store/src/atom.ts

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,18 @@ export function toObserver<T>(
3030
export type WatchedEffect = () => (() => void) | void | undefined
3131

3232
interface WatchableNode extends ReactiveNode {
33+
/**
34+
* Reference count: number of direct subs that are alive
35+
* When >0, the node is "alive" and its watch effects should be running
36+
* When =0, the node is "dead", and its watch effects should be stopped
37+
*/
3338
_watches?: number
3439
_watchEffects?: Array<WatchedEffect>
3540
_watchCleanups?: Array<(() => void) | void | undefined>
3641
}
3742

38-
function getWatchCount(node: WatchableNode): number {
39-
return node._watches || (node.flags & ReactiveFlags.Watching) ? 1 : 0
43+
function isWatched(node: WatchableNode): boolean {
44+
return !!node._watches
4045
}
4146

4247
function addWatch(node: WatchableNode) {
@@ -54,7 +59,7 @@ function addWatch(node: WatchableNode) {
5459
deps = deps.nextDep
5560
}
5661

57-
// 2. start/run watch effects
62+
// 2. start/run own watch effects
5863
const watchEffects = node._watchEffects
5964
if (watchEffects?.length) {
6065
node._watchCleanups = watchEffects.map((ef) => ef())
@@ -82,7 +87,11 @@ function removeWatch(node: WatchableNode) {
8287
}
8388
}
8489

85-
export function whileWatched(node: WatchableNode, fn: WatchedEffect) {
90+
/**
91+
* Causes `fn` to be called when `node` becomes gains its first live subscriber.
92+
* If `fn` returns a cleanup function, it will be called when `node` loses its last live subscriber.
93+
*/
94+
export function whiteWatched(node: WatchableNode, fn: WatchedEffect) {
8695
const initialEffects = (node._watchEffects ??= [])
8796
initialEffects.push(fn)
8897
if (node._watches) {
@@ -157,14 +166,6 @@ const { link: _link, unlink: _unlink, propagate, checkDirty, shallowPropagate }
157166
effect.flags &= ~ReactiveFlags.Watching
158167
},
159168
unwatched(atom: InternalAtom<any>): void {
160-
atom._watched = false
161-
// not sure if this should go above or below the deps purge
162-
// preorder vs postorder
163-
if (atom._watchedCleanups?.length) {
164-
atom._watchedCleanups.forEach((cleanup) => cleanup?.())
165-
atom._watchedCleanups.length = 0
166-
}
167-
168169
if (atom.depsTail !== undefined) {
169170
atom.depsTail = undefined
170171
atom.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty
@@ -178,7 +179,7 @@ function link(dep: ReactiveNode, sub: ReactiveNode, version: number) {
178179
_link(dep, sub, version)
179180
const newTail = dep.subsTail
180181

181-
if (newTail && newTail !== originalTail && getWatchCount(sub)) {
182+
if (newTail && newTail !== originalTail && isWatched(sub)) {
182183
// Propagate watch liveness from sub -> dep
183184
addWatch(dep)
184185
}
@@ -191,7 +192,7 @@ function unlink(
191192
sub: ReactiveNode = link.sub
192193
): Link | undefined {
193194
const dep = link.dep
194-
if (getWatchCount(sub)) {
195+
if (isWatched(sub)) {
195196
// Revoke liveness from this sub on dep when unlinked
196197
removeWatch(dep)
197198
}
@@ -374,21 +375,7 @@ export function createAtom<T>(
374375
},
375376

376377
whileWatched(listener: WatchedEffect): () => void {
377-
atom._watchedSubs ??= []
378-
atom._watchedSubs.push(listener)
379-
if (atom._watched) {
380-
atom._watchedCleanups ??= []
381-
atom._watchedCleanups.push(listener())
382-
}
383-
return () => {
384-
if (!atom._watchedSubs) {
385-
return
386-
}
387-
const index = atom._watchedSubs.indexOf(listener)
388-
if (index !== -1) {
389-
atom._watchedSubs.splice(index, 1)
390-
}
391-
}
378+
return whiteWatched(this, listener)
392379
},
393380

394381
_update(getValue?: T | ((snapshot: T) => T)): boolean {
@@ -471,6 +458,7 @@ export function createAtom<T>(
471458
}
472459

473460
interface Effect extends ReactiveNode {
461+
_watches: number
474462
notify: () => void
475463
stop: () => void
476464
}
@@ -496,6 +484,8 @@ function effect<T>(fn: () => T): Effect {
496484
subs: undefined,
497485
subsTail: undefined,
498486
flags: ReactiveFlags.Watching | ReactiveFlags.RecursedCheck,
487+
// Effects are the source of liveness - they are created alive
488+
_watches: 1,
499489

500490
notify(): void {
501491
const flags = this.flags

packages/store/tests/external-store-atom.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,29 @@ describe('createExternalStoreAtom', () => {
3333
expect(ext.listenerCount()).toBe(0)
3434
})
3535

36+
test('unobserved .get() does not activate the external subscription', () => {
37+
const ext = makeExternalStore(7)
38+
const atom = createExternalStoreAtom(ext.getSnapshot, ext.subscribe)
39+
40+
expect(atom.get()).toBe(7)
41+
expect(ext.listenerCount()).toBe(0)
42+
43+
atom.get()
44+
atom.get()
45+
expect(ext.listenerCount()).toBe(0)
46+
})
47+
48+
test('unobserved chain read through derived atoms does not activate', async () => {
49+
const { createAtom } = await import('../src')
50+
const ext = makeExternalStore(1)
51+
const a = createExternalStoreAtom(ext.getSnapshot, ext.subscribe)
52+
const b = createAtom(() => a.get() * 2)
53+
const c = createAtom(() => `${b.get()} dogs`)
54+
55+
expect(c.get()).toBe('2 dogs')
56+
expect(ext.listenerCount()).toBe(0)
57+
})
58+
3659
test('subscribes on first atom subscriber; unsubscribes on last', () => {
3760
const ext = makeExternalStore(0)
3861
const atom = createExternalStoreAtom(ext.getSnapshot, ext.subscribe)

0 commit comments

Comments
 (0)