@zeix/cause-effect 0.17.3 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.ai-context.md +169 -227
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +176 -116
  4. package/ARCHITECTURE.md +276 -0
  5. package/CHANGELOG.md +29 -0
  6. package/CLAUDE.md +201 -143
  7. package/GUIDE.md +298 -0
  8. package/README.md +246 -193
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/context7.json +4 -0
  12. package/examples/events-sensor.ts +187 -0
  13. package/examples/selector-sensor.ts +173 -0
  14. package/index.dev.js +1390 -1008
  15. package/index.js +1 -1
  16. package/index.ts +60 -74
  17. package/package.json +5 -2
  18. package/skills/changelog-keeper/SKILL.md +59 -0
  19. package/skills/changelog-keeper/agents/openai.yaml +4 -0
  20. package/src/errors.ts +118 -74
  21. package/src/graph.ts +612 -0
  22. package/src/nodes/collection.ts +512 -0
  23. package/src/nodes/effect.ts +149 -0
  24. package/src/nodes/list.ts +589 -0
  25. package/src/nodes/memo.ts +148 -0
  26. package/src/nodes/sensor.ts +149 -0
  27. package/src/nodes/state.ts +135 -0
  28. package/src/nodes/store.ts +378 -0
  29. package/src/nodes/task.ts +174 -0
  30. package/src/signal.ts +112 -66
  31. package/src/util.ts +26 -57
  32. package/test/batch.test.ts +96 -62
  33. package/test/benchmark.test.ts +473 -487
  34. package/test/collection.test.ts +456 -707
  35. package/test/effect.test.ts +293 -696
  36. package/test/list.test.ts +335 -592
  37. package/test/memo.test.ts +574 -0
  38. package/test/regression.test.ts +156 -0
  39. package/test/scope.test.ts +191 -0
  40. package/test/sensor.test.ts +454 -0
  41. package/test/signal.test.ts +220 -213
  42. package/test/state.test.ts +217 -265
  43. package/test/store.test.ts +346 -446
  44. package/test/task.test.ts +529 -0
  45. package/test/untrack.test.ts +167 -0
  46. package/types/index.d.ts +13 -15
  47. package/types/src/errors.d.ts +73 -17
  48. package/types/src/graph.d.ts +218 -0
  49. package/types/src/nodes/collection.d.ts +69 -0
  50. package/types/src/nodes/effect.d.ts +48 -0
  51. package/types/src/nodes/list.d.ts +66 -0
  52. package/types/src/nodes/memo.d.ts +63 -0
  53. package/types/src/nodes/sensor.d.ts +81 -0
  54. package/types/src/nodes/state.d.ts +78 -0
  55. package/types/src/nodes/store.d.ts +51 -0
  56. package/types/src/nodes/task.d.ts +79 -0
  57. package/types/src/signal.d.ts +43 -29
  58. package/types/src/util.d.ts +9 -16
  59. package/archive/benchmark.ts +0 -683
  60. package/archive/collection.ts +0 -253
  61. package/archive/composite.ts +0 -85
  62. package/archive/computed.ts +0 -195
  63. package/archive/list.ts +0 -483
  64. package/archive/memo.ts +0 -139
  65. package/archive/state.ts +0 -90
  66. package/archive/store.ts +0 -298
  67. package/archive/task.ts +0 -189
  68. package/src/classes/collection.ts +0 -245
  69. package/src/classes/computed.ts +0 -349
  70. package/src/classes/list.ts +0 -343
  71. package/src/classes/ref.ts +0 -70
  72. package/src/classes/state.ts +0 -102
  73. package/src/classes/store.ts +0 -262
  74. package/src/diff.ts +0 -138
  75. package/src/effect.ts +0 -93
  76. package/src/match.ts +0 -45
  77. package/src/resolve.ts +0 -49
  78. package/src/system.ts +0 -257
  79. package/test/computed.test.ts +0 -1108
  80. package/test/diff.test.ts +0 -955
  81. package/test/match.test.ts +0 -388
  82. package/test/ref.test.ts +0 -353
  83. package/test/resolve.test.ts +0 -154
  84. package/types/src/classes/collection.d.ts +0 -45
  85. package/types/src/classes/computed.d.ts +0 -94
  86. package/types/src/classes/list.d.ts +0 -43
  87. package/types/src/classes/ref.d.ts +0 -35
  88. package/types/src/classes/state.d.ts +0 -49
  89. package/types/src/classes/store.d.ts +0 -52
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. package/types/src/system.d.ts +0 -78
@@ -0,0 +1,529 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ createEffect,
4
+ createMemo,
5
+ createScope,
6
+ createState,
7
+ createTask,
8
+ isMemo,
9
+ isTask,
10
+ UnsetSignalValueError,
11
+ } from '../index.ts'
12
+
13
+ /* === Utility Functions === */
14
+
15
+ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
16
+
17
+ /* === Tests === */
18
+
19
+ describe('Task', () => {
20
+ describe('createTask', () => {
21
+ test('should resolve async computation', async () => {
22
+ const task = createTask(
23
+ async () => {
24
+ await wait(50)
25
+ return 42
26
+ },
27
+ { value: 0 },
28
+ )
29
+ expect(task.get()).toBe(0)
30
+ await wait(60)
31
+ expect(task.get()).toBe(42)
32
+ })
33
+
34
+ test('should have Symbol.toStringTag of "Task"', () => {
35
+ const task = createTask(async () => 1, { value: 0 })
36
+ expect(task[Symbol.toStringTag]).toBe('Task')
37
+ })
38
+
39
+ test('should throw UnsetSignalValueError before resolution with no initial value', () => {
40
+ const task = createTask(async () => {
41
+ await wait(50)
42
+ return 42
43
+ })
44
+ expect(() => task.get()).toThrow(UnsetSignalValueError)
45
+ })
46
+ })
47
+
48
+ describe('isTask', () => {
49
+ test('should identify task signals', () => {
50
+ expect(isTask(createTask(async () => 1, { value: 0 }))).toBe(true)
51
+ })
52
+
53
+ test('should return false for non-task values', () => {
54
+ expect(isTask(42)).toBe(false)
55
+ expect(isTask(null)).toBe(false)
56
+ expect(isTask({})).toBe(false)
57
+ expect(isMemo(createTask(async () => 1, { value: 0 }))).toBe(false)
58
+ })
59
+ })
60
+
61
+ describe('isPending', () => {
62
+ test('should return true while computation is in-flight', async () => {
63
+ const task = createTask(
64
+ async () => {
65
+ await wait(50)
66
+ return 42
67
+ },
68
+ { value: 0 },
69
+ )
70
+ task.get() // trigger computation
71
+ expect(task.isPending()).toBe(true)
72
+ await wait(60)
73
+ task.get() // read resolved value
74
+ expect(task.isPending()).toBe(false)
75
+ })
76
+
77
+ test('should return false before first get()', () => {
78
+ const task = createTask(async () => 42, { value: 0 })
79
+ expect(task.isPending()).toBe(false)
80
+ })
81
+ })
82
+
83
+ describe('abort', () => {
84
+ test('should abort the current computation', async () => {
85
+ let completed = false
86
+ const task = createTask(
87
+ async (_prev, signal) => {
88
+ await wait(50)
89
+ if (!signal.aborted) completed = true
90
+ return 42
91
+ },
92
+ { value: 0 },
93
+ )
94
+ task.get() // trigger computation
95
+ expect(task.isPending()).toBe(true)
96
+ task.abort()
97
+ expect(task.isPending()).toBe(false)
98
+ await wait(60)
99
+ expect(completed).toBe(false)
100
+ })
101
+ })
102
+
103
+ describe('Dependency Tracking', () => {
104
+ test('should re-execute when dependencies change', async () => {
105
+ const source = createState(1)
106
+ const task = createTask(
107
+ async () => {
108
+ const val = source.get() // dependency tracked before await
109
+ await wait(50)
110
+ return val * 2
111
+ },
112
+ { value: 0 },
113
+ )
114
+
115
+ let result = 0
116
+ createEffect(() => {
117
+ result = task.get()
118
+ })
119
+ expect(result).toBe(0)
120
+ await wait(60)
121
+ expect(result).toBe(2)
122
+
123
+ source.set(5)
124
+ await wait(60)
125
+ expect(result).toBe(10)
126
+ })
127
+
128
+ test('should work with downstream memos', async () => {
129
+ const status = createState('pending')
130
+ const task = createTask(async () => {
131
+ await wait(50)
132
+ status.set('success')
133
+ return 42
134
+ })
135
+ const derived = createMemo(() => {
136
+ try {
137
+ return task.get() + 1
138
+ } catch {
139
+ return 0
140
+ }
141
+ })
142
+ expect(derived.get()).toBe(0)
143
+ expect(status.get()).toBe('pending')
144
+ await wait(60)
145
+ expect(derived.get()).toBe(43)
146
+ expect(status.get()).toBe('success')
147
+ })
148
+
149
+ test('should run tasks in parallel without waterfalls', async () => {
150
+ const a = createTask(
151
+ async () => {
152
+ await wait(80)
153
+ return 10
154
+ },
155
+ { value: 0 },
156
+ )
157
+ const b = createTask(
158
+ async () => {
159
+ await wait(80)
160
+ return 20
161
+ },
162
+ { value: 0 },
163
+ )
164
+ const sum = createMemo(() => a.get() + b.get(), { value: 0 })
165
+ expect(sum.get()).toBe(0)
166
+ await wait(90)
167
+ expect(sum.get()).toBe(30)
168
+ })
169
+ })
170
+
171
+ describe('AbortSignal', () => {
172
+ test('should signal abort when dependency changes during computation', async () => {
173
+ const source = createState(1)
174
+ let wasAborted = false
175
+ const task = createTask(
176
+ async (_prev, signal) => {
177
+ const val = source.get()
178
+ await wait(100)
179
+ if (signal.aborted) wasAborted = true
180
+ return val
181
+ },
182
+ { value: 0 },
183
+ )
184
+
185
+ task.get() // start computation
186
+ await wait(10)
187
+ source.set(2) // change dependency mid-flight
188
+
189
+ await wait(110)
190
+ expect(wasAborted).toBe(true)
191
+ })
192
+
193
+ test('should coalesce multiple rapid changes into one recomputation', async () => {
194
+ const source = createState(1)
195
+ let computationCount = 0
196
+ const task = createTask(
197
+ async () => {
198
+ computationCount++
199
+ await wait(100)
200
+ return source.get()
201
+ },
202
+ { value: 0 },
203
+ )
204
+
205
+ task.get()
206
+ expect(computationCount).toBe(1)
207
+
208
+ source.set(2)
209
+ source.set(3)
210
+ source.set(4)
211
+ await wait(210)
212
+
213
+ expect(task.get()).toBe(4)
214
+ expect(computationCount).toBe(1)
215
+ })
216
+ })
217
+
218
+ describe('Error Handling', () => {
219
+ test('should propagate async errors on get()', async () => {
220
+ const task = createTask(
221
+ async () => {
222
+ await wait(50)
223
+ throw new Error('async failure')
224
+ },
225
+ { value: 0 },
226
+ )
227
+ task.get()
228
+ await wait(60)
229
+ expect(() => task.get()).toThrow('async failure')
230
+ })
231
+
232
+ test('should recover from errors when dependency changes', async () => {
233
+ const source = createState(1)
234
+ const task = createTask(
235
+ async () => {
236
+ const value = source.get()
237
+ await wait(50)
238
+ if (value === 2) throw new Error('bad value')
239
+ return value
240
+ },
241
+ { value: 0 },
242
+ )
243
+
244
+ task.get()
245
+ await wait(60)
246
+ expect(task.get()).toBe(1)
247
+
248
+ source.set(2)
249
+ task.get()
250
+ await wait(60)
251
+ expect(() => task.get()).toThrow('bad value')
252
+
253
+ source.set(3)
254
+ task.get()
255
+ await wait(60)
256
+ expect(task.get()).toBe(3)
257
+ })
258
+ })
259
+
260
+ describe('options.value (prev)', () => {
261
+ test('should return initial value before resolution', () => {
262
+ const task = createTask(
263
+ async () => {
264
+ await wait(50)
265
+ return 42
266
+ },
267
+ { value: 10 },
268
+ )
269
+ expect(task.get()).toBe(10)
270
+ })
271
+
272
+ test('should pass initial value as prev to first computation', async () => {
273
+ let receivedPrev: number | undefined
274
+ const task = createTask(
275
+ async prev => {
276
+ receivedPrev = prev
277
+ await wait(50)
278
+ return prev + 5
279
+ },
280
+ { value: 10 },
281
+ )
282
+
283
+ expect(task.get()).toBe(10)
284
+ await wait(60)
285
+ expect(task.get()).toBe(15)
286
+ expect(receivedPrev).toBe(10)
287
+ })
288
+
289
+ test('should pass previous resolved value on recomputation', async () => {
290
+ const source = createState(1)
291
+ const receivedPrevs: number[] = []
292
+ const task = createTask(
293
+ async prev => {
294
+ const val = source.get() // dependency tracked before await
295
+ receivedPrevs.push(prev)
296
+ await wait(50)
297
+ return val + prev
298
+ },
299
+ { value: 0 },
300
+ )
301
+
302
+ let result = 0
303
+ createEffect(() => {
304
+ result = task.get()
305
+ })
306
+ await wait(60)
307
+ expect(result).toBe(1) // 0 + 1
308
+
309
+ source.set(2)
310
+ await wait(60)
311
+ expect(result).toBe(3) // 1 + 2
312
+ expect(receivedPrevs).toEqual([0, 1])
313
+ })
314
+ })
315
+
316
+ describe('options.equals', () => {
317
+ test('should use custom equality to skip propagation after resolution', async () => {
318
+ const source = createState(1)
319
+ let effectCount = 0
320
+ const task = createTask(
321
+ async () => {
322
+ const val = source.get() // dependency tracked before await
323
+ await wait(50)
324
+ return { x: val % 2 }
325
+ },
326
+ {
327
+ value: { x: -1 },
328
+ equals: (a, b) => a.x === b.x,
329
+ },
330
+ )
331
+
332
+ createEffect(() => {
333
+ task.get()
334
+ effectCount++
335
+ })
336
+ await wait(60) // first resolution: { x: 1 }
337
+
338
+ source.set(3) // still odd — result will be { x: 1 }, structurally equal
339
+ await wait(60)
340
+ const countAfterEqual = effectCount
341
+
342
+ source.set(2) // now even — result will be { x: 0 }, different
343
+ await wait(60)
344
+
345
+ // After the structurally different result resolves, effect should run again
346
+ expect(effectCount).toBeGreaterThan(countAfterEqual)
347
+ })
348
+ })
349
+
350
+ describe('options.guard', () => {
351
+ test('should validate initial value against guard', () => {
352
+ expect(() => {
353
+ createTask(async () => 42, {
354
+ // @ts-expect-error - Testing invalid input
355
+ value: 'foo',
356
+ guard: (v): v is number => typeof v === 'number',
357
+ })
358
+ }).toThrow('[Task] Signal value "foo" is invalid')
359
+ })
360
+
361
+ test('should accept initial value that passes guard', () => {
362
+ const task = createTask(async () => 42, {
363
+ value: 10,
364
+ guard: (v): v is number => typeof v === 'number',
365
+ })
366
+ expect(task.get()).toBe(10)
367
+ })
368
+ })
369
+
370
+ describe('Input Validation', () => {
371
+ test('should throw InvalidCallbackError for sync callback', () => {
372
+ expect(() => {
373
+ // @ts-expect-error - Testing invalid input
374
+ createTask((_a: unknown) => 42)
375
+ }).toThrow('[Task] Callback (_a) => 42 is invalid')
376
+ })
377
+
378
+ test('should throw InvalidCallbackError for non-function callback', () => {
379
+ // @ts-expect-error - Testing invalid input
380
+ expect(() => createTask(null)).toThrow(
381
+ '[Task] Callback null is invalid',
382
+ )
383
+ // @ts-expect-error - Testing invalid input
384
+ expect(() => createTask(42)).toThrow(
385
+ '[Task] Callback 42 is invalid',
386
+ )
387
+ })
388
+
389
+ test('should throw NullishSignalValueError for null initial value', () => {
390
+ expect(() => {
391
+ // @ts-expect-error - Testing invalid input
392
+ createTask(async () => 42, { value: null })
393
+ }).toThrow('[Task] Signal value cannot be null or undefined')
394
+ })
395
+ })
396
+
397
+ describe('options.watched', () => {
398
+ test('should call watched on first effect access', () => {
399
+ let watchedCount = 0
400
+
401
+ const task = createTask(
402
+ async () => {
403
+ await wait(10)
404
+ return 1
405
+ },
406
+ {
407
+ value: 0,
408
+ watched: _invalidate => {
409
+ watchedCount++
410
+ return () => {}
411
+ },
412
+ },
413
+ )
414
+
415
+ expect(watchedCount).toBe(0)
416
+
417
+ const dispose = createScope(() => {
418
+ createEffect(() => {
419
+ void task.get()
420
+ })
421
+ })
422
+
423
+ expect(watchedCount).toBe(1)
424
+ dispose()
425
+ })
426
+
427
+ test('should call cleanup when last effect stops watching', () => {
428
+ let cleanedUp = false
429
+
430
+ const task = createTask(
431
+ async () => {
432
+ await wait(10)
433
+ return 1
434
+ },
435
+ {
436
+ value: 0,
437
+ watched: _invalidate => {
438
+ return () => {
439
+ cleanedUp = true
440
+ }
441
+ },
442
+ },
443
+ )
444
+
445
+ const dispose = createScope(() => {
446
+ createEffect(() => {
447
+ void task.get()
448
+ })
449
+ })
450
+
451
+ expect(cleanedUp).toBe(false)
452
+ dispose()
453
+ expect(cleanedUp).toBe(true)
454
+ })
455
+
456
+ test('should re-execute task when invalidate is called', async () => {
457
+ let externalValue = 10
458
+ let computeCount = 0
459
+ let invalidate!: () => void
460
+
461
+ const task = createTask(
462
+ async () => {
463
+ computeCount++
464
+ await wait(10)
465
+ return externalValue
466
+ },
467
+ {
468
+ value: 0,
469
+ watched: inv => {
470
+ invalidate = inv
471
+ return () => {}
472
+ },
473
+ },
474
+ )
475
+
476
+ let observed = 0
477
+ const dispose = createScope(() => {
478
+ createEffect(() => {
479
+ observed = task.get()
480
+ })
481
+ })
482
+
483
+ await wait(20)
484
+ expect(observed).toBe(10)
485
+ expect(computeCount).toBe(1)
486
+
487
+ externalValue = 20
488
+ invalidate()
489
+ await wait(20)
490
+ expect(observed).toBe(20)
491
+ expect(computeCount).toBe(2)
492
+
493
+ dispose()
494
+ })
495
+
496
+ test('should abort in-flight task when invalidate is called', async () => {
497
+ let wasAborted = false
498
+ let invalidate!: () => void
499
+
500
+ const task = createTask(
501
+ async (_prev, signal) => {
502
+ await wait(100)
503
+ if (signal.aborted) wasAborted = true
504
+ return 1
505
+ },
506
+ {
507
+ value: 0,
508
+ watched: inv => {
509
+ invalidate = inv
510
+ return () => {}
511
+ },
512
+ },
513
+ )
514
+
515
+ const dispose = createScope(() => {
516
+ createEffect(() => {
517
+ void task.get()
518
+ })
519
+ })
520
+
521
+ await wait(10) // task is in-flight
522
+ invalidate() // should trigger re-execution, aborting the current one
523
+ await wait(110)
524
+ expect(wasAborted).toBe(true)
525
+
526
+ dispose()
527
+ })
528
+ })
529
+ })
@@ -0,0 +1,167 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ createEffect,
4
+ createMemo,
5
+ createScope,
6
+ createState,
7
+ untrack,
8
+ } from '../index.ts'
9
+
10
+ /* === Tests === */
11
+
12
+ describe('untrack', () => {
13
+ test('should return the value of the callback', () => {
14
+ const result = untrack(() => 42)
15
+ expect(result).toBe(42)
16
+ })
17
+
18
+ test('should read a signal without tracking it', () => {
19
+ const tracked = createState('tracked')
20
+ const untracked = createState('untracked')
21
+ let count = 0
22
+ createEffect((): undefined => {
23
+ tracked.get()
24
+ untrack(() => untracked.get())
25
+ count++
26
+ })
27
+ expect(count).toBe(1)
28
+
29
+ // changing tracked signal should re-run the effect
30
+ tracked.set('changed')
31
+ expect(count).toBe(2)
32
+
33
+ // changing untracked signal should not re-run the effect
34
+ untracked.set('changed')
35
+ expect(count).toBe(2)
36
+ })
37
+
38
+ test('should not track dependencies in memos', () => {
39
+ const a = createState(1)
40
+ const b = createState(2)
41
+ const sum = createMemo(() => a.get() + untrack(() => b.get()))
42
+ expect(sum.get()).toBe(3)
43
+
44
+ // changing a should recompute
45
+ a.set(10)
46
+ expect(sum.get()).toBe(12)
47
+
48
+ // changing b should not recompute (stale value of b used)
49
+ b.set(20)
50
+ expect(sum.get()).toBe(12)
51
+ })
52
+
53
+ test('should prevent dependency pollution from subcomponent creation', () => {
54
+ const parentSignal = createState('parent')
55
+ let parentRuns = 0
56
+ let childRuns = 0
57
+
58
+ const dispose = createScope(() => {
59
+ createEffect((): undefined => {
60
+ parentSignal.get()
61
+ parentRuns++
62
+
63
+ // Simulate subcomponent: create local state + effect
64
+ // Without untrack, childSignal.get() in the child effect
65
+ // would link to the parent effect during initial run
66
+ untrack(() => {
67
+ const childSignal = createState('child')
68
+ createEffect((): undefined => {
69
+ childSignal.get()
70
+ childRuns++
71
+ })
72
+ childSignal.set('updated')
73
+ })
74
+ })
75
+ })
76
+
77
+ expect(parentRuns).toBe(1)
78
+ expect(childRuns).toBe(2) // initial + update
79
+
80
+ // parent should re-run when its own signal changes
81
+ parentSignal.set('changed')
82
+ expect(parentRuns).toBe(2)
83
+
84
+ dispose()
85
+ })
86
+
87
+ test('should prevent parent effect from re-running on child signal changes', () => {
88
+ const show = createState(true)
89
+ let parentRuns = 0
90
+ let childValue = ''
91
+
92
+ const dispose = createScope(() => {
93
+ createEffect((): undefined => {
94
+ parentRuns++
95
+ if (show.get()) {
96
+ // Subcomponent with its own reactive state
97
+ untrack(() => {
98
+ const label = createState('hello')
99
+ createEffect((): undefined => {
100
+ childValue = label.get()
101
+ })
102
+ label.set('world')
103
+ })
104
+ }
105
+ })
106
+ })
107
+
108
+ expect(parentRuns).toBe(1)
109
+ expect(childValue).toBe('world')
110
+
111
+ // toggling show re-runs parent (it's tracked)
112
+ show.set(false)
113
+ expect(parentRuns).toBe(2)
114
+
115
+ dispose()
116
+ })
117
+
118
+ test('should nest correctly', () => {
119
+ const a = createState(1)
120
+ const b = createState(2)
121
+ const c = createState(3)
122
+ let count = 0
123
+ createEffect((): undefined => {
124
+ a.get()
125
+ untrack(() => {
126
+ b.get()
127
+ untrack(() => {
128
+ c.get()
129
+ })
130
+ })
131
+ count++
132
+ })
133
+ expect(count).toBe(1)
134
+
135
+ a.set(10)
136
+ expect(count).toBe(2)
137
+
138
+ b.set(20)
139
+ expect(count).toBe(2)
140
+
141
+ c.set(30)
142
+ expect(count).toBe(2)
143
+ })
144
+
145
+ test('should restore tracking after untrack completes', () => {
146
+ const before = createState('before')
147
+ const during = createState('during')
148
+ const after = createState('after')
149
+ let count = 0
150
+ createEffect((): undefined => {
151
+ before.get()
152
+ untrack(() => during.get())
153
+ after.get()
154
+ count++
155
+ })
156
+ expect(count).toBe(1)
157
+
158
+ before.set('x')
159
+ expect(count).toBe(2)
160
+
161
+ during.set('x')
162
+ expect(count).toBe(2)
163
+
164
+ after.set('x')
165
+ expect(count).toBe(3)
166
+ })
167
+ })