1import { addEqualityTesters, afterEach, assert, beforeEach, describe, expect, it, test, vitest } from "@effect/vitest"
2import {
3 Array as Arr,
4 Cause,
5 Context,
6 Effect,
7 Hash,
8 Latch,
9 Layer,
10 Option,
11 Queue,
12 Result,
13 Schema,
14 Stream,
15 SubscriptionRef
16} from "effect"
17import { KeyValueStore } from "effect/persistence"
18import { AsyncResult, Atom, AtomRegistry, Hydration, Reactivity } from "effect/reactivity"
19import { TestClock } from "effect/testing"
20import { collectGarbage, getGc } from "../utils/gc.ts"
21
22declare const global: any
23
24addEqualityTesters()
25
26describe("Atom", { concurrent: false }, () => {
27 beforeEach(async () => {
28 vitest.useFakeTimers({
29 toFake: [
30 "Date",
31 "hrtime",
32 "setTimeout",
33 "clearTimeout",
34 "setInterval",
35 "clearInterval",
36 "performance"
37 ]
38 })
39 await Effect.runPromise(Effect.yieldNow)
40 })
41 afterEach(() => {
42 vitest.useRealTimers()
43 })
44
45 it("get/set", () => {
46 const counter = Atom.make(0)
47 const r = AtomRegistry.make()
48 expect(r.get(counter)).toEqual(0)
49 r.set(counter, 1)
50 expect(r.get(counter)).toEqual(1)
51 })
52
53 it("keepAlive false", async () => {
54 const counter = Atom.make(0)
55 const r = AtomRegistry.make()
56 r.set(counter, 1)
57 expect(r.get(counter)).toEqual(1)
58 await Effect.runPromise(Effect.yieldNow)
59 expect(r.get(counter)).toEqual(0)
60 })
61
62 it("keepAlive true", async () => {
63 const counter = Atom.make(0).pipe(
64 Atom.keepAlive
65 )
66 const r = AtomRegistry.make()
67 r.set(counter, 1)
68 expect(r.get(counter)).toEqual(1)
69 await new Promise((resolve) => resolve(null))
70 expect(r.get(counter)).toEqual(1)
71 })
72
73 it("subscribe", async () => {
74 const counter = Atom.make(0)
75 const r = AtomRegistry.make()
76 let count = 0
77 const cancel = r.subscribe(counter, (_) => {
78 count = _
79 })
80 r.set(counter, 1)
81 expect(count).toEqual(1)
82 await Effect.runPromise(Effect.yieldNow)
83
84 expect(r.get(counter)).toEqual(1)
85 cancel()
86 await Effect.runPromise(Effect.yieldNow)
87 expect(r.get(counter)).toEqual(0)
88 })
89
90 it("subscribe does not skip listeners when unsubscribing during notify", () => {
91 const counter = Atom.make(0)
92 const r = AtomRegistry.make()
93 let first = 0
94 let second = 0
95 let cancelFirst = () => {
96 }
97
98 cancelFirst = r.subscribe(counter, () => {
99 first++
100 cancelFirst()
101 })
102
103 r.subscribe(counter, () => {
104 second++
105 })
106
107 r.set(counter, 1)
108
109 expect(first).toEqual(1)
110 expect(second).toEqual(1)
111 })
112
113 it("withEquality skips notifications for equivalent values", () => {
114 const point = Atom.make({ x: 0, y: 0 }).pipe(
115 Atom.withEquality<{ x: number; y: number }>((a, b) => a.x === b.x && a.y === b.y),
116 Atom.keepAlive
117 )
118 const r = AtomRegistry.make()
119 const initial = r.get(point)
120 let count = 0
121 r.subscribe(point, () => {
122 count++
123 })
124
125 r.set(point, { x: 0, y: 0 })
126 expect(count).toEqual(0)
127 expect(r.get(point)).toBe(initial)
128
129 r.set(point, { x: 1, y: 0 })
130 expect(count).toEqual(1)
131 expect(r.get(point)).toEqual({ x: 1, y: 0 })
132 })
133
134 it("withEquality skips invalidation of derived atoms", () => {
135 const point = Atom.make({ x: 0, y: 0 }).pipe(
136 Atom.withEquality<{ x: number; y: number }>((a, b) => a.x === b.x && a.y === b.y),
137 Atom.keepAlive
138 )
139 let builds = 0
140 const x = Atom.map(point, (p) => {
141 builds++
142 return p.x
143 })
144 const r = AtomRegistry.make()
145 r.subscribe(x, () => {})
146
147 expect(r.get(x)).toEqual(0)
148 expect(builds).toEqual(1)
149 r.set(point, { x: 0, y: 0 })
150 expect(builds).toEqual(1)
151 r.set(point, { x: 2, y: 0 })
152 expect(r.get(x)).toEqual(2)
153 expect(builds).toEqual(2)
154 })
155
156 it("searchParam with schema reads initial query value", () => {
157 const previousWindow = (globalThis as any).window
158 const r = AtomRegistry.make()
159 Object.defineProperty(globalThis, "window", {
160 value: {
161 location: {
162 pathname: "/",
163 search: "?page=6"
164 },
165 history: {
166 pushState: () => {
167 }
168 },
169 addEventListener: () => {
170 },
171 removeEventListener: () => {
172 }
173 },
174 configurable: true,
175 writable: true
176 })
177
178 try {
179 const page = Atom.searchParam("page", { schema: Schema.NumberFromString })
180 expect(r.get(page)).toEqual(Option.some(6))
181 } finally {
182 r.dispose()
183 if (typeof previousWindow === "undefined") {
184 delete (globalThis as any).window
185 } else {
186 Object.defineProperty(globalThis, "window", {
187 value: previousWindow,
188 configurable: true,
189 writable: true
190 })
191 }
192 }
193 })
194
195 it("runtime", async () => {
196 const count = counterRuntime.atom(Counter.use((_) => _.get)).pipe(
197 Atom.withLabel("count")
198 )
199 const r = AtomRegistry.make()
200 const result = r.get(count)
201 assert(AsyncResult.isSuccess(result))
202 expect(result.value).toEqual(1)
203 })
204
205 it("runtime layers are disposed with their registry", () => {
206 interface Service {
207 readonly id: number
208 readonly isAlive: () => boolean
209 readonly finalize: () => void
210 }
211 const Service = Context.Service<Service>("Atom.test/RegistryScopedService")
212 const finalized: Array<number> = []
213 let builds = 0
214 const layer = Layer.effect(
215 Service,
216 Effect.acquireRelease(
217 Effect.sync(() => {
218 const id = ++builds
219 let alive = true
220 return Service.of({ id, isAlive: () => alive, finalize: () => alive = false })
221 }),
222 (service) =>
223 Effect.sync(() => {
224 service.finalize()
225 finalized.push(service.id)
226 })
227 )
228 )
229 const runtime = Atom.runtime(layer)
230 const service = runtime.atom(Service)
231 const registryA = AtomRegistry.make()
232 const registryB = AtomRegistry.make()
233
234 const resultA = registryA.get(service)
235 const resultB = registryB.get(service)
236 assert(AsyncResult.isSuccess(resultA))
237 assert(AsyncResult.isSuccess(resultB))
238 expect(resultA.value.id).not.toEqual(resultB.value.id)
239
240 registryA.dispose()
241
242 expect(finalized).toEqual([resultA.value.id])
243 expect(resultA.value.isAlive()).toEqual(false)
244 expect(resultB.value.isAlive()).toEqual(true)
245 assert(AsyncResult.isSuccess(registryB.get(service)))
246
247 registryB.dispose()
248 })
249
250 it("default runtime factories build layers once per registry", () => {
251 const Service = Context.Service<number>("Atom.test/DefaultRegistryScopedService")
252 let builds = 0
253 const runtime = Atom.runtime(Layer.sync(Service, () => ++builds))
254 const service = runtime.atom(Service)
255 const registryA = AtomRegistry.make()
256 const registryB = AtomRegistry.make()
257
258 expect(registryA.get(service)).toEqual(AsyncResult.success(1))
259 expect(registryB.get(service)).toEqual(AsyncResult.success(2))
260 expect(builds).toEqual(2)
261
262 registryA.dispose()
263 registryB.dispose()
264 })
265
266 it("concrete runtime memo maps share layers across registries", () => {
267 const Service = Context.Service<number>("Atom.test/SharedRuntimeService")
268 let builds = 0
269 const factory = Atom.context({ memoMap: Layer.makeMemoMapUnsafe() })
270 const runtime = factory(Layer.sync(Service, () => ++builds))
271 const service = runtime.atom(Service)
272 const registryA = AtomRegistry.make()
273 const registryB = AtomRegistry.make()
274
275 expect(registryA.get(service)).toEqual(AsyncResult.success(1))
276 expect(registryB.get(service)).toEqual(AsyncResult.success(1))
277 expect(builds).toEqual(1)
278
279 registryA.dispose()
280 registryB.dispose()
281 })
282
283 it("shared memo map atoms share within a registry and isolate across registries", () => {
284 const Service = Context.Service<number>("Atom.test/SharedRegistryScopedService")
285 let builds = 0
286 const layer = Layer.sync(Service, () => ++builds)
287 const memoMap = Atom.make(() => Layer.makeMemoMapUnsafe())
288 const factoryA = Atom.context({ memoMap })
289 const factoryB = Atom.context({ memoMap })
290 const serviceA = factoryA(layer).atom(Service)
291 const serviceB = factoryB(layer).atom(Service)
292 const registryA = AtomRegistry.make()
293 const registryB = AtomRegistry.make()
294
295 expect(registryA.get(serviceA)).toEqual(AsyncResult.success(1))
296 expect(registryA.get(serviceB)).toEqual(AsyncResult.success(1))
297 expect(registryB.get(serviceA)).toEqual(AsyncResult.success(2))
298 expect(registryB.get(serviceB)).toEqual(AsyncResult.success(2))
299 expect(builds).toEqual(2)
300
301 registryA.dispose()
302 registryB.dispose()
303 })
304
305 it("runtime replacement", async () => {
306 const count = counterRuntime.atom(Counter.use((_) => _.get))
307 const r = AtomRegistry.make({
308 initialValues: [Atom.initialValue(counterRuntime.layer, CounterTest)]
309 })
310 const result = r.get(count)
311 assert(AsyncResult.isSuccess(result))
312 expect(result.value).toEqual(10)
313 })
314
315 it("runtime replacement", async () => {
316 const count = Atom.fnSync<number, number>((x) => x, { initialValue: 0 })
317 const r = AtomRegistry.make({ initialValues: [Atom.initialValue(count, 10)] })
318 const result = r.get(count)
319 expect(result).toEqual(10)
320 r.set(count, 20)
321 const result2 = r.get(count)
322 expect(result2).toEqual(20)
323 })
324
325 it("runtime multiple", async () => {
326 const buildCount = buildCounterRuntime.fn<void>()((_) => BuildCounter.use((_) => _.get))
327 const count = counterRuntime.atom(Counter.use((_) => _.get))
328 const timesTwo = multiplierRuntime.atom((get) =>
329 Effect.gen(function*() {
330 const counter = yield* Counter
331 const multiplier = yield* Multiplier
332 yield* counter.inc
333 expect(yield* get.result(count)).toEqual(2)
334 return yield* multiplier.times(2)
335 })
336 )
337 const r = AtomRegistry.make()
338 const cancel = r.mount(buildCount)
339
340 let result = r.get(timesTwo)
341 assert(AsyncResult.isSuccess(result))
342 expect(result.value).toEqual(4)
343
344 result = r.get(count)
345 assert(AsyncResult.isSuccess(result))
346 expect(result.value).toEqual(2)
347
348 r.set(buildCount, void 0)
349 assert.deepStrictEqual(r.get(buildCount), AsyncResult.success(1))
350
351 await Effect.runPromise(Effect.yieldNow)
352 await Effect.runPromise(Effect.yieldNow)
353
354 result = r.get(count)
355 assert(AsyncResult.isSuccess(result))
356 expect(result.value).toEqual(1)
357
358 r.set(buildCount, void 0)
359 assert.deepStrictEqual(r.get(buildCount), AsyncResult.success(2))
360
361 cancel()
362 })
363
364 it("runtime direct tag", async () => {
365 const counter = counterRuntime.atom(Counter)
366 const r = AtomRegistry.make()
367 const result = r.get(counter)
368 assert(AsyncResult.isSuccess(result))
369 assert(Effect.isEffect(result.value.get))
370 })
371
372 it("effect initial", async () => {
373 const count = Atom.make(
374 Effect.succeed(1).pipe(Effect.delay(100)),
375 { initialValue: 0 }
376 ).pipe(Atom.keepAlive)
377 const r = AtomRegistry.make()
378 let result = r.get(count)
379 assert(AsyncResult.isSuccess(result))
380 assert.strictEqual(result.value, 0)
381
382 await vitest.advanceTimersByTimeAsync(100)
383 result = r.get(count)
384 assert(AsyncResult.isSuccess(result))
385 expect(result.value).toEqual(1)
386 })
387
388 it("effectFn", async () => {
389 const count = Atom.fn((n: number) => Effect.succeed(n + 1))
390 const r = AtomRegistry.make()
391 let result = r.get(count)
392 assert(AsyncResult.isInitial(result))
393 r.set(count, 1)
394 result = r.get(count)
395 assert(AsyncResult.isSuccess(result))
396 expect(result.value).toEqual(2)
397 })
398
399 it("effectFn concurrent", async () => {
400 const latches = Arr.empty<Latch.Latch>()
401 let done = 0
402 const count = Atom.fn((_: number) => {
403 const latch = Latch.makeUnsafe()
404 latches.push(latch)
405 return latch.await.pipe(
406 Effect.tap(() => Effect.sync(() => done++))
407 )
408 }, { concurrent: true })
409
410 const r = AtomRegistry.make()
411 r.mount(count)
412
413 let result = r.get(count)
414 assert(AsyncResult.isInitial(result))
415
416 r.set(count, 1)
417 result = r.get(count)
418 assert(AsyncResult.isInitial(result) && result.waiting)
419
420 r.set(count, 1)
421 r.set(count, 1)
422 assert(AsyncResult.isInitial(result) && result.waiting)
423 assert.strictEqual(latches.length, 3)
424 assert.strictEqual(done, 0)
425
426 latches.forEach((latch) => latch.openUnsafe())
427 await Effect.runPromise(Effect.yieldNow)
428 assert.strictEqual(done, 3)
429
430 result = r.get(count)
431 assert(AsyncResult.isSuccess(result))
432 })
433
434 it("effectFn concurrent preserves synchronous success and failure", () => {
435 const count = Atom.fn((n: number) => n === 1 ? Effect.succeed(n + 1) : Effect.fail("fail"), {
436 concurrent: true
437 })
438 const r = AtomRegistry.make()
439 r.mount(count)
440
441 r.set(count, 1)
442 const success = r.get(count)
443 assert(AsyncResult.isSuccess(success))
444 assert.strictEqual(success.value, 2)
445
446 r.set(count, 2)
447 const failure = r.get(count)
448 assert(AsyncResult.isFailure(failure))
449 const error = Cause.findErrorOption(failure.cause)
450 assert(Option.isSome(error))
451 assert.strictEqual(error.value, "fail")
452 r.dispose()
453 })
454
455 it("effectFn concurrent waits for earlier calls", async () => {
456 const latch = Latch.makeUnsafe()
457 const count = Atom.fn((n: number) => n === 1 ? latch.await.pipe(Effect.as(n)) : Effect.succeed(n), {
458 concurrent: true
459 })
460 const r = AtomRegistry.make()
461 r.mount(count)
462
463 r.set(count, 1)
464 assert(AsyncResult.isInitial(r.get(count)))
465 r.set(count, 2)
466 const waiting = r.get(count)
467 assert(AsyncResult.isInitial(waiting) && waiting.waiting)
468
469 latch.openUnsafe()
470 await Effect.runPromise(Effect.yieldNow)
471 const result = r.get(count)
472 assert(AsyncResult.isSuccess(result))
473 assert.strictEqual(result.value, 1)
474 r.dispose()
475 })
476
477 it("effectFn concurrent observes an earlier failure after a later success", async () => {
478 const latch = Latch.makeUnsafe()
479 const count = Atom.fn((n: number) =>
480 n === 1
481 ? latch.await.pipe(Effect.flatMap(() => Effect.fail("older failure")))
482 : Effect.succeed(n), { concurrent: true })
483 const r = AtomRegistry.make()
484 r.mount(count)
485
486 r.set(count, 1)
487 r.set(count, 2)
488 const waiting = r.get(count)
489 assert(AsyncResult.isInitial(waiting) && waiting.waiting)
490
491 latch.openUnsafe()
492 await Effect.runPromise(Effect.yieldNow)
493 const result = r.get(count)
494 assert(AsyncResult.isFailure(result))
495 const error = Cause.findErrorOption(result.cause)
496 assert(Option.isSome(error))
497 assert.strictEqual(error.value, "older failure")
498 r.dispose()
499 })
500
501 it("effectFn initial", async () => {
502 const count = Atom.fn((n: number) => Effect.succeed(n + 1), {
503 initialValue: 0
504 })
505 const r = AtomRegistry.make()
506 let result = r.get(count)
507 assert(AsyncResult.isSuccess(result))
508 assert.strictEqual(result.value, 0)
509 r.set(count, 1)
510 result = r.get(count)
511 assert(AsyncResult.isSuccess(result))
512 expect(result.value).toEqual(2)
513 })
514
515 it("effect mapResult", async () => {
516 const count = Atom.fn((n: number) => Effect.succeed(n + 1)).pipe(
517 Atom.mapResult((_) => _ + 1)
518 )
519 const r = AtomRegistry.make()
520 let result = r.get(count)
521 assert(AsyncResult.isInitial(result))
522 r.set(count, 1)
523 result = r.get(count)
524 assert(AsyncResult.isSuccess(result))
525 expect(result.value).toEqual(3)
526 })
527
528 it("effect double mapResult", async () => {
529 const seed = Atom.make(0)
530 const count = Atom.make((get) => Effect.succeed(get(seed) + 1)).pipe(
531 Atom.mapResult((_) => _ + 10),
532 Atom.mapResult((_) => _ + 100)
533 )
534 const r = AtomRegistry.make()
535 let result = r.get(count)
536 assert(AsyncResult.isSuccess(result))
537 expect(result.value).toEqual(111)
538 r.set(seed, 1)
539 result = r.get(count)
540 assert(AsyncResult.isSuccess(result))
541 expect(result.value).toEqual(112)
542 })
543
544 it("effect double mapResult refresh", async () => {
545 let rebuilds = 0
546 const count = Atom.make(() => {
547 rebuilds++
548 return Effect.succeed(1)
549 }).pipe(
550 Atom.mapResult((_) => _ + 10),
551 Atom.mapResult((_) => _ + 100)
552 )
553 const r = AtomRegistry.make()
554 let result = r.get(count)
555 assert(AsyncResult.isSuccess(result))
556 expect(result.value).toEqual(111)
557 expect(rebuilds).toEqual(1)
558 r.refresh(count)
559 result = r.get(count)
560 assert(AsyncResult.isSuccess(result))
561 expect(result.value).toEqual(111)
562 expect(rebuilds).toEqual(2)
563 })
564
565 it("keeps parent child links when a parent is read more than once", () => {
566 const flag = Atom.make(true)
567 const base = Atom.make(0)
568 const derived = Atom.make((get) => {
569 const value = get(base)
570 if (get(flag)) {
571 get(base)
572 }
573 return value
574 })
575 const registry = AtomRegistry.make()
576 const unsubscribe = registry.subscribe(derived, () => {
577 }, { immediate: true })
578 const nodes = registry.getNodes()
579 const baseNode = nodes.get(base)
580 const derivedNode = nodes.get(derived)
581
582 assert(baseNode !== undefined)
583 assert(derivedNode !== undefined)
584 assert.strictEqual(baseNode.children.has(derivedNode), true)
585 assert.strictEqual(derivedNode.parents.has(baseNode), true)
586
587 registry.set(flag, false)
588
589 assert.strictEqual(baseNode.children.has(derivedNode), true)
590 assert.strictEqual(derivedNode.parents.has(baseNode), true)
591
592 registry.set(base, 1)
593
594 assert.strictEqual(registry.get(derived), 1)
595 unsubscribe()
596 })
597
598 it("a stale dependent keeps its dependency", async () => {
599 const a = Atom.make(0)
600 let builds = 0
601 const dependency = Atom.make(() => ++builds)
602 const dependent = Atom.make((get) => get(a) + get(dependency)).pipe(Atom.keepAlive)
603 const r = AtomRegistry.make()
604 r.get(dependent)
605 r.set(a, 1)
606 await Effect.runPromise(Effect.yieldNow)
607 assert.strictEqual(r.get(dependent), 2)
608 })
609
610 it("a listener added to a stale node hears changes through a parent that rebuilds to the same value", () => {
611 const a = Atom.make(0)
612 const b = Atom.make(0)
613 const middle = Atom.make((get) => get(a) > 0 ? 1 : 0)
614 const derived = Atom.make((get) => get(middle) + get(b))
615 const r = AtomRegistry.make()
616 r.get(derived)
617 r.set(b, 5)
618 r.set(a, 1)
619 const seen: Array<number> = []
620 r.subscribe(derived, (value) => seen.push(value))
621 r.set(a, 0)
622 assert.deepStrictEqual(seen, [5])
623 })
624
625 it("recovers an observed derived atom after its build throws", () => {
626 const source = Atom.make(0)
627 const derived = Atom.make((get) => {
628 const value = get(source)
629 if (value === 1) throw new Error("build failed")
630 return value
631 })
632 const r = AtomRegistry.make()
633 const sourceValues: Array<number> = []
634 const derivedValues: Array<number> = []
635
636 assert.strictEqual(r.get(derived), 0)
637 r.subscribe(source, (value) => sourceValues.push(value))
638 r.subscribe(derived, (value) => derivedValues.push(value))
639
640 assert.throws(() => r.set(source, 1), /build failed/)
641 assert.deepStrictEqual(sourceValues, [1])
642
643 r.set(source, 2)
644 assert.deepStrictEqual(derivedValues, [2])
645 })
646
647 it("a build superseded while it runs is released", () => {
648 const p = Atom.make(0)
649 let finalized = 0
650 const n = Atom.make((get) => {
651 get.addFinalizer(() => finalized++)
652 if (get(p) === 0) get.set(p, 1)
653 return get(p)
654 })
655 const r = AtomRegistry.make()
656 r.subscribe(n, () => {})
657 r.get(n)
658 assert.strictEqual(finalized, 1)
659 })
660
661 it("refresh derived before mount resolves base effect", async () => {
662 const baseAtom = Atom.make(
663 Effect.succeed("value").pipe(Effect.delay(100))
664 )
665 const derivedAtom = Atom.writable(
666 (get) => get(baseAtom),
667 () => {
668 },
669 (refresh) => refresh(baseAtom)
670 )
671 const registry = AtomRegistry.make()
672
673 registry.refresh(derivedAtom)
674 const unmount = registry.mount(derivedAtom)
675
676 let result = registry.get(derivedAtom)
677 assert(result.waiting)
678
679 await vitest.advanceTimersByTimeAsync(100)
680
681 result = registry.get(derivedAtom)
682 assert(AsyncResult.isSuccess(result))
683 expect(result.value).toEqual("value")
684
685 unmount()
686 })
687
688 it("scopedFn", async () => {
689 let finalized = 0
690 const count = Atom.fn((n: number) =>
691 Effect.succeed(n + 1).pipe(
692 Effect.tap(
693 Effect.addFinalizer(() =>
694 Effect.sync(() => {
695 finalized++
696 })
697 )
698 )
699 )
700 ).pipe(Atom.keepAlive)
701 const r = AtomRegistry.make()
702 let result = r.get(count)
703 assert(AsyncResult.isInitial(result))
704
705 await new Promise((resolve) => resolve(null))
706 expect(finalized).toEqual(0)
707
708 r.set(count, 1)
709 result = r.get(count)
710 assert(AsyncResult.isSuccess(result))
711 expect(result.value).toEqual(2)
712
713 r.set(count, 2)
714 await new Promise((resolve) => resolve(null))
715 expect(finalized).toEqual(1)
716 })
717
718 it("disposed lifetime apis are no-ops", () => {
719 let context: Atom.AtomContext | undefined
720 const state = Atom.make(0).pipe(Atom.keepAlive)
721 const option = Atom.make<Option.Option<number>>(Option.some(1)).pipe(Atom.keepAlive)
722 const result = Atom.make<AsyncResult.AsyncResult<number, never>>(AsyncResult.success(1)).pipe(Atom.keepAlive)
723 const atom = Atom.make((get) => {
724 context = get
725 return get(state)
726 }).pipe(Atom.keepAlive)
727 const registry = AtomRegistry.make()
728
729 expect(registry.get(atom)).toEqual(0)
730 registry.refresh(atom)
731
732 assert(context !== undefined)
733 const disposed = context
734
735 expect(() =>
736 disposed.addFinalizer(() => {
737 })
738 ).not.toThrow()
739 expect(disposed(state)).toEqual(0)
740 expect(disposed.get(state)).toEqual(0)
741 expect(disposed.once(state)).toEqual(0)
742 expect(disposed.self<number>()).toEqual(Option.none())
743 expect(() => disposed.result(result)).not.toThrow()
744 expect(() => disposed.resultOnce(result)).not.toThrow()
745 expect(() => disposed.setResult(result, AsyncResult.success(2))).not.toThrow()
746 expect(() => disposed.some(option)).not.toThrow()
747 expect(() => disposed.someOnce(option)).not.toThrow()
748 expect(() => disposed.refresh(state)).not.toThrow()
749 expect(() => disposed.refreshSelf()).not.toThrow()
750 expect(() => disposed.mount(state)).not.toThrow()
751 expect(() =>
752 disposed.subscribe(state, () => {
753 })
754 ).not.toThrow()
755 expect(() => disposed.setSelf(1)).not.toThrow()
756 expect(() => disposed.set(state, 1)).not.toThrow()
757 expect(registry.get(state)).toEqual(0)
758 expect(() => disposed.stream(state)).not.toThrow()
759 expect(() => disposed.streamResult(result)).not.toThrow()
760 })
761
762 it("disposed lifetime ignores async updates", async () => {
763 const count = Atom.make(
764 Effect.succeed(1).pipe(Effect.delay(100)),
765 {
766 initialValue: 0,
767 uninterruptible: true
768 }
769 )
770 const registry = AtomRegistry.make()
771 const unmount = registry.mount(count)
772
773 const initial = registry.get(count)
774 assert(AsyncResult.isSuccess(initial))
775 expect(initial.waiting).toEqual(true)
776 expect(initial.value).toEqual(0)
777
778 unmount()
779 await vitest.advanceTimersByTimeAsync(100)
780 await Effect.runPromise(Effect.yieldNow)
781
782 const next = registry.get(count)
783 assert(AsyncResult.isSuccess(next))
784 expect(next.waiting).toEqual(true)
785 })
786
787 it.effect("stream", () =>
788 Effect.gen(function*() {
789 vitest.useRealTimers()
790
791 const services = yield* Effect.context<never>()
792 const count = Atom.make(
793 Stream.range(0, 2).pipe(
794 Stream.tap(() => AtomRegistry.AtomRegistry),
795 Stream.tap((_) => Effect.sleep(50)),
796 Stream.provideContext(services)
797 )
798 )
799 const r = AtomRegistry.make()
800 const unmount = r.mount(count)
801 let result = r.get(count)
802 assert(result.waiting)
803 assert(AsyncResult.isInitial(result))
804
805 yield* TestClock.adjust(50)
806 result = r.get(count)
807 assert(result.waiting)
808 assert(AsyncResult.isSuccess(result))
809 assert.deepEqual(result.value, 0)
810
811 yield* TestClock.adjust(50)
812 result = r.get(count)
813 assert(result.waiting)
814 assert(AsyncResult.isSuccess(result))
815 assert.deepEqual(result.value, 1)
816
817 yield* TestClock.adjust(50)
818 result = r.get(count)
819 assert(!result.waiting)
820 assert(AsyncResult.isSuccess(result))
821 assert.deepEqual(result.value, 2)
822
823 unmount()
824 yield* TestClock.adjust(50)
825 result = r.get(count)
826 assert(result.waiting)
827 assert(AsyncResult.isInitial(result))
828 }))
829
830 it("stream initial", async () => {
831 const count = Atom.make(
832 Stream.range(1, 2).pipe(
833 Stream.tap(() => Effect.sleep(50))
834 ),
835 { initialValue: 0 }
836 )
837 const r = AtomRegistry.make()
838 const unmount = r.mount(count)
839 let result = r.get(count)
840 assert(result.waiting)
841 assert(AsyncResult.isSuccess(result))
842 assert.strictEqual(result.value, 0)
843
844 await vitest.advanceTimersByTimeAsync(50)
845 result = r.get(count)
846 assert(result.waiting)
847 assert(AsyncResult.isSuccess(result))
848 assert.deepEqual(result.value, 1)
849
850 unmount()
851 await new Promise((resolve) => resolve(null))
852 result = r.get(count)
853 assert(result.waiting)
854 assert(AsyncResult.isSuccess(result))
855 })
856
857 it("streamFn", async () => {
858 const count = Atom.fn((start: number) =>
859 Stream.range(start, start + 1).pipe(
860 Stream.tap(() => Effect.sleep(50))
861 )
862 )
863 const r = AtomRegistry.make()
864 const unmount = r.mount(count)
865 let result = r.get(count)
866 assert.strictEqual(result._tag, "Initial")
867
868 await vitest.advanceTimersByTimeAsync(50)
869 result = r.get(count)
870 assert.strictEqual(result._tag, "Initial")
871
872 r.set(count, 1)
873 result = r.get(count)
874 assert(result.waiting)
875 assert.strictEqual(result._tag, "Initial")
876
877 await vitest.advanceTimersByTimeAsync(50)
878 result = r.get(count)
879 assert(result.waiting)
880 assert(AsyncResult.isSuccess(result))
881 assert.deepEqual(result.value, 1)
882
883 await vitest.advanceTimersByTimeAsync(50)
884 result = r.get(count)
885 assert(!result.waiting)
886 assert(AsyncResult.isSuccess(result))
887 assert.deepEqual(result.value, 2)
888
889 r.set(count, 5)
890 result = r.get(count)
891 assert(result.waiting)
892 assert(AsyncResult.isSuccess(result))
893 assert.deepEqual(result.value, 2)
894
895 await vitest.advanceTimersByTimeAsync(50)
896 result = r.get(count)
897 assert(result.waiting)
898 assert(AsyncResult.isSuccess(result))
899 assert.deepEqual(result.value, 5)
900
901 await vitest.advanceTimersByTimeAsync(50)
902 result = r.get(count)
903 assert(!result.waiting)
904 assert(AsyncResult.isSuccess(result))
905 assert.deepEqual(result.value, 6)
906
907 unmount()
908 await Effect.runPromise(Effect.yieldNow)
909 result = r.get(count)
910 assert(AsyncResult.isInitial(result))
911 })
912
913 it("pull", async () => {
914 const count = Atom.pull(
915 Stream.range(0, 1, 1).pipe(
916 Stream.tap(() => Effect.sleep(50))
917 )
918 )
919 const r = AtomRegistry.make()
920 const unmount = r.mount(count)
921
922 let result = r.get(count)
923 assert(result.waiting)
924 assert(Option.isNone(AsyncResult.value(result)))
925
926 await vitest.advanceTimersByTimeAsync(50)
927 result = r.get(count)
928 assert(!result.waiting)
929 assert(AsyncResult.isSuccess(result))
930 assert.deepEqual(result.value, { done: false, items: [0] })
931
932 r.set(count, void 0)
933 result = r.get(count)
934 assert(result.waiting)
935 assert.deepEqual(AsyncResult.value(result), Option.some({ done: false, items: [0] }))
936
937 await vitest.advanceTimersByTimeAsync(50)
938 result = r.get(count)
939 assert(!result.waiting)
940 assert(AsyncResult.isSuccess(result))
941 assert.deepEqual(result.value, { done: false, items: [0, 1] })
942
943 r.set(count, void 0)
944 result = r.get(count)
945 assert(!result.waiting)
946 assert(AsyncResult.isSuccess(result))
947 assert.deepEqual(result.value, { done: true, items: [0, 1] })
948
949 r.refresh(count)
950 result = r.get(count)
951 assert(result.waiting)
952 assert.deepEqual(AsyncResult.value(result), Option.some({ done: true, items: [0, 1] }))
953
954 await vitest.advanceTimersByTimeAsync(50)
955 result = r.get(count)
956 assert(!result.waiting)
957 assert(AsyncResult.isSuccess(result))
958 assert.deepEqual(result.value, { done: false, items: [0] })
959
960 unmount()
961 await Effect.runPromise(Effect.yieldNow)
962 result = r.get(count)
963 assert(result.waiting)
964 assert(Option.isNone(AsyncResult.value(result)))
965 })
966
967 it("pull runtime", async () => {
968 const count = counterRuntime.pull(
969 Counter.use((_) => _.get).pipe(
970 Effect.map((_) => Stream.range(_, 2, 1)),
971 Stream.unwrap,
972 Stream.tap(() => Effect.sleep(50))
973 )
974 )
975 const r = AtomRegistry.make()
976 const unmount = r.mount(count)
977
978 let result = r.get(count)
979 assert(result.waiting)
980 assert(Option.isNone(AsyncResult.value(result)))
981
982 await vitest.advanceTimersByTimeAsync(50)
983 result = r.get(count)
984 assert(!result.waiting)
985 assert(AsyncResult.isSuccess(result))
986 assert.deepEqual(result.value, { done: false, items: [1] })
987
988 r.set(count, void 0)
989 result = r.get(count)
990 assert(result.waiting)
991 assert.deepEqual(AsyncResult.value(result), Option.some({ done: false, items: [1] }))
992
993 await vitest.advanceTimersByTimeAsync(50)
994 result = r.get(count)
995 assert(!result.waiting)
996 assert(AsyncResult.isSuccess(result))
997 assert.deepEqual(result.value, { done: false, items: [1, 2] })
998
999 r.set(count, void 0)
1000 result = r.get(count)
1001 assert(!result.waiting)
1002 assert(AsyncResult.isSuccess(result))
1003 assert.deepEqual(result.value, { done: true, items: [1, 2] })
1004
1005 r.refresh(count)
1006 result = r.get(count)
1007 assert(result.waiting)
1008 assert.deepEqual(AsyncResult.value(result), Option.some({ done: true, items: [1, 2] }))
1009
1010 await vitest.advanceTimersByTimeAsync(50)
1011 result = r.get(count)
1012 assert(!result.waiting)
1013 assert(AsyncResult.isSuccess(result))
1014 assert.deepEqual(result.value, { done: false, items: [1] })
1015
1016 unmount()
1017 await Effect.runPromise(Effect.yieldNow)
1018 result = r.get(count)
1019 assert(result.waiting)
1020 assert(Option.isNone(AsyncResult.value(result)))
1021 })
1022
1023 it("pull refreshable", async () => {
1024 const count = Atom.pull(() =>
1025 Stream.range(1, 2, 1).pipe(
1026 Stream.tap(() => Effect.sleep(50))
1027 )
1028 )
1029 const r = AtomRegistry.make()
1030 const unmount = r.mount(count)
1031
1032 let result = r.get(count)
1033 assert(result.waiting)
1034 assert(AsyncResult.isInitial(result))
1035
1036 await vitest.advanceTimersByTimeAsync(50)
1037 result = r.get(count)
1038 assert(!result.waiting)
1039 assert(AsyncResult.isSuccess(result))
1040 assert.deepEqual(result.value, { done: false, items: [1] })
1041
1042 unmount()
1043 await Effect.runPromise(Effect.yieldNow)
1044 result = r.get(count)
1045 assert(result.waiting)
1046 })
1047
1048 it("family", async () => {
1049 const r = AtomRegistry.make()
1050
1051 const count = Atom.family((n: number) => Atom.make(n))
1052 const hash = Hash.hash(count(1))
1053 assert.strictEqual(count(1), count(1))
1054 r.set(count(1), 2)
1055 assert.strictEqual(r.get(count(1)), 2)
1056
1057 const countKeep = Atom.family((n: number) => Atom.make(n).pipe(Atom.keepAlive))
1058 assert.strictEqual(countKeep(1), countKeep(1))
1059 r.get(countKeep(1))
1060 const hashKeep = Hash.hash(countKeep(1))
1061
1062 if (global.gc) {
1063 vitest.useRealTimers()
1064 await new Promise((resolve) => setTimeout(resolve, 0))
1065 global.gc()
1066 assert.notEqual(hash, Hash.hash(count(1)))
1067 assert.strictEqual(hashKeep, Hash.hash(countKeep(1)))
1068 }
1069 })
1070
1071 it.skipIf(process.versions.bun !== undefined || process.versions.deno !== undefined)(
1072 "family keeps a replacement atom when the collected atom is finalized late",
1073 async () => {
1074 vitest.useRealTimers()
1075 const gc = await getGc()
1076 const tick = () => new Promise((resolve) => setTimeout(resolve, 0))
1077 const family = Atom.family((_: string) => Atom.make(0))
1078
1079 const older = new WeakRef(family("a"))
1080 for (let i = 0; i < 8 && older.deref() !== undefined; i++) {
1081 await tick()
1082 gc()
1083 }
1084 assert.isUndefined(older.deref())
1085
1086 // Replace the collected atom before its finalizer gets a turn.
1087 const current = family("a")
1088 await Effect.runPromise(collectGarbage)
1089
1090 assert.strictEqual(family("a"), current)
1091 }
1092 )
1093
1094 it("label", async () => {
1095 expect(
1096 Atom.make(0).pipe(Atom.withLabel("counter")).label![1]
1097 ).toMatch(/Atom.test.ts:\d+:\d+/)
1098 })
1099
1100 it("batching", async () => {
1101 const r = AtomRegistry.make()
1102 const state = Atom.make(1).pipe(Atom.keepAlive)
1103 const state2 = Atom.make("a").pipe(Atom.keepAlive)
1104 let count = 0
1105 const derived = Atom.readable((get) => {
1106 count++
1107 return get(state) + get(state2)
1108 })
1109 expect(r.get(derived)).toEqual("1a")
1110 expect(count).toEqual(1)
1111 Atom.batch(() => {
1112 r.set(state, 2)
1113 r.set(state2, "b")
1114 })
1115 expect(count).toEqual(2)
1116 expect(r.get(derived)).toEqual("2b")
1117 })
1118
1119 it.effect("retains method-form dependencies added during a batch rebuild", () =>
1120 Effect.gen(function*() {
1121 const registry = AtomRegistry.make()
1122 const source = Atom.make(Option.none<string>())
1123 const gate = yield* Latch.make()
1124 const asyncAtom = Atom.make((get) =>
1125 Effect.gen(function*() {
1126 const value = get(source)
1127 if (Option.isNone(value)) {
1128 return yield* Effect.fail("SourceIsNone" as const)
1129 }
1130 yield* gate.await
1131 return `computed-${value.value}`
1132 })
1133 )
1134 const derived = Atom.make((get): unknown => {
1135 const value = get.get(source)
1136 if (Option.isNone(value)) {
1137 return "empty"
1138 }
1139 return get.get(asyncAtom)
1140 })
1141
1142 registry.subscribe(derived, () => {}, { immediate: true })
1143 registry.subscribe(asyncAtom, () => {}, { immediate: true })
1144
1145 Atom.batch(() => registry.set(source, Option.some("a")))
1146
1147 yield* gate.open
1148 yield* Effect.yieldNow
1149
1150 const result = registry.get(derived) as AsyncResult.AsyncResult<string, "SourceIsNone">
1151 assert(AsyncResult.isSuccess(result))
1152 assert.strictEqual(result.value, "computed-a")
1153 }))
1154
1155 it("rebuilds an atom invalidated during its own batch rebuild", () => {
1156 const registry = AtomRegistry.make()
1157 const source = Atom.make(0)
1158 const enabled = Atom.make(false)
1159 const updateSource = Atom.make((get) => {
1160 get.set(source, 1)
1161 })
1162 const derived = Atom.make((get) => {
1163 const value = get(source)
1164 if (get(enabled)) {
1165 get(updateSource)
1166 }
1167 return value
1168 })
1169
1170 registry.subscribe(derived, () => {}, { immediate: true })
1171
1172 Atom.batch(() => registry.set(enabled, true))
1173
1174 assert.strictEqual(registry.get(derived), 1)
1175 })
1176
1177 it("nested batch", async () => {
1178 const r = AtomRegistry.make()
1179 const state = Atom.make(1).pipe(Atom.keepAlive)
1180 const state2 = Atom.make("a").pipe(Atom.keepAlive)
1181 let count = 0
1182 const derived = Atom.readable((get) => {
1183 count++
1184 return get(state) + get(state2)
1185 })
1186 expect(r.get(derived)).toEqual("1a")
1187 expect(count).toEqual(1)
1188 Atom.batch(() => {
1189 r.set(state, 2)
1190 Atom.batch(() => {
1191 r.set(state2, "b")
1192 })
1193 })
1194 expect(count).toEqual(2)
1195 expect(r.get(derived)).toEqual("2b")
1196 })
1197
1198 it("read correct updated state in batch", async () => {
1199 const r = AtomRegistry.make()
1200 const state = Atom.make(1).pipe(Atom.keepAlive)
1201 const state2 = Atom.make("a").pipe(Atom.keepAlive)
1202 let count = 0
1203 const derived = Atom.readable((get) => {
1204 count++
1205 return get(state) + get(state2)
1206 })
1207 expect(r.get(derived)).toEqual("1a")
1208 expect(count).toEqual(1)
1209 Atom.batch(() => {
1210 r.set(state, 2)
1211 expect(r.get(derived)).toEqual("2a")
1212 r.set(state2, "b")
1213 })
1214 expect(count).toEqual(3)
1215 expect(r.get(derived)).toEqual("2b")
1216 expect(count).toEqual(3)
1217 })
1218
1219 it("notifies listeners after batch commit", async () => {
1220 const r = AtomRegistry.make()
1221 const state = Atom.make(1).pipe(Atom.keepAlive)
1222 const state2 = Atom.make("a").pipe(Atom.keepAlive)
1223 let count = 0
1224 const derived = Atom.readable((get) => {
1225 return get(state) + get(state2)
1226 })
1227 r.subscribe(derived, () => {
1228 count++
1229 })
1230 Atom.batch(() => {
1231 r.get(derived)
1232 r.set(state, 2)
1233 r.get(derived)
1234 r.set(state2, "b")
1235 })
1236 expect(count).toEqual(1)
1237 expect(r.get(derived)).toEqual("2b")
1238 })
1239
1240 it.each([
1241 { existingListener: false, expected: [0] },
1242 { existingListener: true, expected: [0, 0] }
1243 ])("delivers a batched value to a late subscriber (existing listener: $existingListener)", ({
1244 existingListener,
1245 expected
1246 }) => {
1247 const r = AtomRegistry.make()
1248 const state = Atom.make(existingListener ? 1 : 0)
1249 const seen: Array<number> = []
1250
1251 Atom.batch(() => {
1252 if (existingListener) {
1253 r.subscribe(state, () => {})
1254 r.set(state, 0)
1255 } else {
1256 r.get(state)
1257 }
1258 r.subscribe(state, (value) => seen.push(value), { immediate: true })
1259 })
1260
1261 assert.deepStrictEqual(seen, expected)
1262 r.dispose()
1263 })
1264
1265 it("does not queue an initialValues notification without listeners", () => {
1266 const state = Atom.make(0)
1267 const seen: Array<number> = []
1268
1269 Atom.batch(() => {
1270 const r = AtomRegistry.make({ initialValues: [Atom.initialValue(state, 10)] })
1271 r.subscribe(state, (value) => seen.push(value), { immediate: true })
1272 })
1273
1274 assert.deepStrictEqual(seen, [10])
1275 })
1276
1277 it("runs Atom.fn writes from batch commit listeners", () => {
1278 const registry = AtomRegistry.make()
1279 const source = Atom.make(0)
1280 const write = Atom.fn((value: number, get) => Effect.sync(() => get.registry.set(source, value)))
1281 const seen: Array<number> = []
1282 registry.mount(write)
1283 registry.subscribe(source, (value) => seen.push(value))
1284 registry.subscribe(source, (value) => {
1285 if (value < 3) registry.set(write, value + 1)
1286 })
1287
1288 Atom.batch(() => registry.set(source, 1))
1289
1290 assert.deepStrictEqual(seen, [1, 2, 3])
1291 registry.dispose()
1292 })
1293
1294 it("initialValues", async () => {
1295 const state = Atom.make(0)
1296 const r = AtomRegistry.make({
1297 initialValues: [
1298 Atom.initialValue(state, 10)
1299 ]
1300 })
1301 expect(r.get(state)).toEqual(10)
1302 await Effect.runPromise(Effect.yieldNow)
1303 expect(r.get(state)).toEqual(0)
1304 })
1305
1306 it("map with initialValue still rerenders when source changes", () => {
1307 const state = Atom.make(0).pipe(Atom.keepAlive)
1308 const mapped = state.pipe(
1309 Atom.map((n) => n + 1),
1310 Atom.keepAlive
1311 )
1312 const r = AtomRegistry.make({
1313 initialValues: [
1314 Atom.initialValue(mapped, 10)
1315 ]
1316 })
1317 r.mount(mapped)
1318
1319 assert.strictEqual(r.get(mapped), 10)
1320
1321 r.set(state, 1)
1322
1323 assert.strictEqual(r.get(mapped), 2)
1324 })
1325
1326 it("idleTTL", async () => {
1327 const state = Atom.make(0)
1328 const state2 = Atom.make(0).pipe(
1329 Atom.setIdleTTL(10000)
1330 )
1331 const state3 = Atom.make(0).pipe(
1332 Atom.setIdleTTL(3000)
1333 )
1334 const r = AtomRegistry.make({ defaultIdleTTL: 2000 })
1335 r.set(state, 10)
1336 r.set(state2, 10)
1337 r.set(state3, 10)
1338 expect(r.get(state)).toEqual(10)
1339 expect(r.get(state2)).toEqual(10)
1340 expect(r.get(state3)).toEqual(10)
1341 await new Promise((resolve) => resolve(null))
1342 expect(r.get(state)).toEqual(10)
1343 expect(r.get(state2)).toEqual(10)
1344 expect(r.get(state3)).toEqual(10)
1345
1346 await new Promise((resolve) => resolve(null))
1347 await vitest.advanceTimersByTimeAsync(10000)
1348 expect(r.get(state)).toEqual(0)
1349 expect(r.get(state2)).toEqual(10)
1350 expect(r.get(state3)).toEqual(0)
1351
1352 await new Promise((resolve) => resolve(null))
1353 await vitest.advanceTimersByTimeAsync(20000)
1354 expect(r.get(state)).toEqual(0)
1355 expect(r.get(state2)).toEqual(0)
1356 expect(r.get(state3)).toEqual(0)
1357 })
1358
1359 it("releases a stream-backed atom consumed through a derived atom after idleTTL", async () => {
1360 let released = false
1361 const source = Stream.callback<number>((queue) =>
1362 Effect.acquireRelease(
1363 Effect.sync(() => setInterval(() => Queue.offerUnsafe(queue, Date.now()), 100)),
1364 (handle) =>
1365 Effect.sync(() => {
1366 released = true
1367 clearInterval(handle)
1368 })
1369 ).pipe(Effect.asVoid)
1370 )
1371 const feed = Atom.make(() => source)
1372 const derived = Atom.make((get) => AsyncResult.isSuccess(get(feed)) ? "ready" : "initial")
1373 const r = AtomRegistry.make({ defaultIdleTTL: 400 })
1374
1375 try {
1376 const unsubscribe = r.subscribe(derived, () => {}, { immediate: true })
1377 await vitest.advanceTimersByTimeAsync(600)
1378 unsubscribe()
1379
1380 await vitest.advanceTimersByTimeAsync(2000)
1381
1382 expect(released).toEqual(true)
1383 } finally {
1384 r.dispose()
1385 }
1386 })
1387
1388 it("idleTTL fn", async () => {
1389 const fn = Atom.fn((n: number) => Effect.succeed(n + 1)).pipe(
1390 Atom.setIdleTTL(0)
1391 )
1392 const r = AtomRegistry.make({ defaultIdleTTL: 2000 })
1393
1394 let result = r.get(fn)
1395 assert(AsyncResult.isInitial(result))
1396
1397 r.set(fn, 1)
1398 result = r.get(fn)
1399 assert(AsyncResult.isSuccess(result))
1400 expect(result.value).toEqual(2)
1401
1402 await Effect.runPromise(Effect.yieldNow)
1403
1404 result = r.get(fn)
1405 assert(AsyncResult.isInitial(result))
1406 })
1407
1408 it("idleTTL fnSync", async () => {
1409 const fn = Atom.fnSync((n: number) => n + 1).pipe(
1410 Atom.setIdleTTL(0)
1411 )
1412 const r = AtomRegistry.make({ defaultIdleTTL: 2000 })
1413
1414 let result = r.get(fn)
1415 assert(Option.isNone(result))
1416
1417 r.set(fn, 1)
1418 result = r.get(fn)
1419 assert(Option.isSome(result))
1420 expect(result.value).toEqual(2)
1421
1422 await Effect.runPromise(Effect.yieldNow)
1423
1424 result = r.get(fn)
1425 assert(Option.isNone(result))
1426 })
1427
1428 it("fn", async () => {
1429 const count = Atom.fnSync((n: number) => n).pipe(Atom.keepAlive)
1430 const r = AtomRegistry.make()
1431 assert.deepEqual(r.get(count), Option.none())
1432
1433 r.set(count, 1)
1434 assert.deepEqual(r.get(count), Option.some(1))
1435 })
1436
1437 it("fn initial", async () => {
1438 const count = Atom.fnSync((n: number) => n, { initialValue: 0 })
1439 const r = AtomRegistry.make()
1440 assert.deepEqual(r.get(count), 0)
1441
1442 r.set(count, 1)
1443 assert.deepEqual(r.get(count), 1)
1444 })
1445
1446 it("withFallback", async () => {
1447 const count = Atom.make(() =>
1448 Effect.succeed(1).pipe(
1449 Effect.delay(100)
1450 )
1451 ).pipe(
1452 Atom.withFallback(Atom.make(() => Effect.succeed(0))),
1453 Atom.keepAlive
1454 )
1455 const r = AtomRegistry.make()
1456 assert.deepEqual(r.get(count), AsyncResult.waiting(AsyncResult.success(0)))
1457
1458 await vitest.advanceTimersByTimeAsync(100)
1459 assert.deepEqual(r.get(count), AsyncResult.success(1))
1460 })
1461
1462 it("withFallback forwards writes to the primary atom", () => {
1463 const primary = Atom.make<AsyncResult.AsyncResult<number>>(AsyncResult.success(1))
1464 const fallback = AsyncResult.success(2)
1465 const atom = Atom.withFallback(primary, Atom.make(fallback))
1466 const r = AtomRegistry.make()
1467
1468 r.set(atom, AsyncResult.initial())
1469
1470 assert.deepEqual([r.get(primary), r.get(atom)], [
1471 AsyncResult.initial(),
1472 AsyncResult.waiting(fallback)
1473 ])
1474 })
1475
1476 it("failure with previousSuccess", async () => {
1477 const count = Atom.fn((i: number) => i === 1 ? Effect.fail("fail") : Effect.succeed(i))
1478 const r = AtomRegistry.make()
1479
1480 let result = r.get(count)
1481 assert(AsyncResult.isInitial(result))
1482
1483 r.set(count, 0)
1484 result = r.get(count)
1485 assert(AsyncResult.isSuccess(result))
1486 assert.strictEqual(result.value, 0)
1487
1488 r.set(count, 1)
1489 result = r.get(count)
1490 assert(AsyncResult.isFailure(result))
1491 const error = Cause.findErrorOption(result.cause)
1492 assert(Option.isSome(error))
1493 assert.strictEqual(error.value, "fail")
1494
1495 const value = AsyncResult.value(result)
1496 assert(Option.isSome(value))
1497 assert.strictEqual(value.value, 0)
1498 })
1499
1500 it("read non-object", () => {
1501 const bool = Atom.make(() => true)
1502 const r = AtomRegistry.make()
1503 assert.strictEqual(r.get(bool), true)
1504 })
1505
1506 it("get.stream", async () => {
1507 const count = Atom.make(0)
1508 const multiplied = Atom.make((get) => get.stream(count).pipe(Stream.map((_) => _ * 2)))
1509
1510 const r = AtomRegistry.make()
1511 const cancel = r.mount(multiplied)
1512
1513 assert.strictEqual(r.get(count), 0)
1514 assert.deepStrictEqual(r.get(multiplied), AsyncResult.success(0, { waiting: true }))
1515
1516 r.set(count, 1)
1517 await Effect.runPromise(Effect.yieldNow)
1518 assert.deepStrictEqual(r.get(multiplied), AsyncResult.success(2, { waiting: true }))
1519
1520 cancel()
1521 })
1522
1523 it("get.streamResult", async () => {
1524 const count = Atom.make(0)
1525 const multiplied = Atom.make((get) => get.stream(count).pipe(Stream.map((_) => _ * 2)))
1526 const plusOne = Atom.make((get) => get.streamResult(multiplied).pipe(Stream.map((_) => _ + 1)))
1527
1528 const r = AtomRegistry.make()
1529 const cancel = r.mount(plusOne)
1530
1531 assert.strictEqual(r.get(count), 0)
1532 assert.deepStrictEqual(r.get(plusOne), AsyncResult.success(1, { waiting: true }))
1533
1534 r.set(count, 1)
1535 await Effect.runPromise(Effect.yieldNow)
1536 await Effect.runPromise(Effect.yieldNow)
1537 assert.deepStrictEqual(r.get(plusOne), AsyncResult.success(3, { waiting: true }))
1538
1539 cancel()
1540 })
1541
1542 it("stream failure keeps previousSuccess", async () => {
1543 const atom = Atom.make(() => Stream.succeed(1).pipe(Stream.concat(Stream.fail("boom"))))
1544 const r = AtomRegistry.make()
1545 const cancel = r.mount(atom)
1546
1547 await new Promise((resolve) => resolve(null))
1548 const afterFail = r.get(atom)
1549 assert(AsyncResult.isFailure(afterFail))
1550 const prev = AsyncResult.value(afterFail)
1551 assert(Option.isSome(prev))
1552 assert.strictEqual(prev.value, 1)
1553
1554 cancel()
1555 })
1556
1557 it("stream empty produces NoSuchElementError", async () => {
1558 const atom = Atom.make(Stream.empty) satisfies Atom.Atom<
1559 AsyncResult.AsyncResult<never, Cause.NoSuchElementError>
1560 >
1561 const r = AtomRegistry.make()
1562 const cancel = r.mount(atom)
1563
1564 await vitest.advanceTimersByTimeAsync(0)
1565 const result = r.get(atom)
1566 assert(AsyncResult.isFailure(result))
1567 assert.deepStrictEqual(
1568 AsyncResult.error(result),
1569 Option.some(new Cause.NoSuchElementError())
1570 )
1571
1572 cancel()
1573 })
1574
1575 it("Option is not an Effect", async () => {
1576 const atom = Atom.make(Option.none<string>())
1577 const r = AtomRegistry.make()
1578 assert.deepStrictEqual(r.get(atom), Option.none())
1579 })
1580
1581 it("Either is not an Effect", async () => {
1582 const atom = Atom.make(Result.succeed(123))
1583 const r = AtomRegistry.make()
1584 assert.deepStrictEqual(r.get(atom), Result.succeed(123))
1585 })
1586
1587 it("SubscriptionRef", async () => {
1588 vitest.useRealTimers()
1589 const ref = SubscriptionRef.make(123).pipe(Effect.runSync)
1590 const atom = Atom.subscriptionRef(ref)
1591 const r = AtomRegistry.make()
1592 assert.deepStrictEqual(r.get(atom), 123)
1593 await Effect.runPromise(SubscriptionRef.update(ref, (a) => a + 1))
1594 assert.deepStrictEqual(r.get(atom), 124)
1595 })
1596
1597 it("SubscriptionRef", async () => {
1598 vitest.useRealTimers()
1599 const ref = SubscriptionRef.make(0).pipe(Effect.runSync)
1600 const atom = Atom.subscriptionRef(ref)
1601 const r = AtomRegistry.make()
1602 const unmount = r.mount(atom)
1603 assert.deepStrictEqual(r.get(atom), 0)
1604 r.set(atom, 1)
1605 await new Promise((resolve) => resolve(null))
1606 assert.deepStrictEqual(r.get(atom), 1)
1607 unmount()
1608 })
1609
1610 it("SubscriptionRef/effect", async () => {
1611 const atom = Atom.subscriptionRef(SubscriptionRef.make(0))
1612 const r = AtomRegistry.make()
1613 const unmount = r.mount(atom)
1614 assert.deepStrictEqual(r.get(atom), AsyncResult.success(0, { waiting: true }))
1615 r.set(atom, 1)
1616 await new Promise((resolve) => resolve(null))
1617 assert.deepStrictEqual(r.get(atom), AsyncResult.success(1, { waiting: true }))
1618 unmount()
1619 })
1620
1621 it("SubscriptionRef/runtime", async () => {
1622 const atom = counterRuntime.subscriptionRef(SubscriptionRef.make(0))
1623 const r = AtomRegistry.make()
1624 const unmount = r.mount(atom)
1625 assert.deepStrictEqual(r.get(atom), AsyncResult.success(0, { waiting: true }))
1626 r.set(atom, 1)
1627 await new Promise((resolve) => resolve(null))
1628 assert.deepStrictEqual(r.get(atom), AsyncResult.success(1, { waiting: true }))
1629 unmount()
1630 })
1631
1632 it("SubscriptionRef/runtime/scoped", async () => {
1633 let finalized = false
1634 const atom = counterRuntime.subscriptionRef(
1635 Effect.gen(function*() {
1636 yield* Effect.addFinalizer(() =>
1637 Effect.sync(() => {
1638 finalized = true
1639 })
1640 )
1641 return yield* SubscriptionRef.make(0)
1642 })
1643 )
1644 const r = AtomRegistry.make()
1645 const unmount = r.mount(atom)
1646 assert.deepStrictEqual(r.get(atom), AsyncResult.success(0, { waiting: true }))
1647 r.set(atom, 1)
1648 await new Promise((resolve) => resolve(null))
1649 assert.deepStrictEqual(r.get(atom), AsyncResult.success(1, { waiting: true }))
1650 assert.strictEqual(finalized, false)
1651 unmount()
1652 await Effect.runPromise(Effect.yieldNow)
1653 assert.strictEqual(finalized, true)
1654 })
1655
1656 it("setLazy(true)", async () => {
1657 const count = Atom.make(0).pipe(Atom.keepAlive)
1658 let rebuilds = 0
1659 const double = Atom.make((get) => {
1660 rebuilds++
1661 return get(count) * 2
1662 }).pipe(Atom.keepAlive)
1663 const r = AtomRegistry.make()
1664 assert.strictEqual(r.get(double), 0)
1665 r.set(count, 1)
1666 assert.strictEqual(rebuilds, 1)
1667 assert.strictEqual(r.get(double), 2)
1668 assert.strictEqual(rebuilds, 2)
1669 })
1670
1671 it("setLazy(false)", async () => {
1672 const count = Atom.make(0).pipe(Atom.keepAlive)
1673 let rebuilds = 0
1674 const double = Atom.make((get) => {
1675 rebuilds++
1676 return get(count) * 2
1677 }).pipe(Atom.setLazy(false), Atom.keepAlive)
1678 const r = AtomRegistry.make()
1679 assert.strictEqual(r.get(double), 0)
1680 r.set(count, 1)
1681 assert.strictEqual(rebuilds, 2)
1682 assert.strictEqual(r.get(double), 2)
1683 assert.strictEqual(rebuilds, 2)
1684 })
1685
1686 it("derived derived with with effect result", async () => {
1687 const r = AtomRegistry.make()
1688 const state = Atom.fn(Effect.succeed<number>)
1689 let count = 0
1690 const derived = Atom.readable((get) => {
1691 count++
1692 return get(state).pipe(AsyncResult.getOrElse(() => -1)) % 3
1693 })
1694 let count2 = 0
1695 const derived2 = Atom.readable((get) => {
1696 count2++
1697 return get(derived) + 10
1698 })
1699 const cancel = r.mount(derived2)
1700
1701 expect(r.get(derived)).toEqual(-1)
1702 expect(count).toEqual(1)
1703 expect(r.get(derived2)).toEqual(9)
1704 expect(count2).toEqual(1)
1705 r.set(state, 2)
1706 expect(r.get(derived)).toEqual(2)
1707 expect(count).toEqual(2)
1708 expect(r.get(derived2)).toEqual(12)
1709 expect(count2).toEqual(2)
1710 r.set(state, 5)
1711 expect(r.get(derived)).toEqual(2)
1712 expect(count).toEqual(3)
1713 expect(r.get(derived2)).toEqual(12)
1714 expect(count2).toEqual(2)
1715 cancel()
1716 })
1717
1718 test(`toStreamResult`, async () => {
1719 const r = AtomRegistry.make()
1720 const atom = Atom.make(Effect.succeed(1))
1721 const eff = Atom.toStreamResult(atom).pipe(
1722 Stream.runHead,
1723 Effect.provideService(AtomRegistry.AtomRegistry, r)
1724 )
1725 const result = await Effect.runPromise(eff)
1726 expect(Option.getOrThrow(result)).toEqual(1)
1727 })
1728
1729 test(`refreshOnSignal`, async () => {
1730 const r = AtomRegistry.make()
1731 let rebuilds = 0
1732 const signal = Atom.make(0)
1733 const refreshOnSignal = Atom.makeRefreshOnSignal(signal)
1734 const atom = Atom.make(() => {
1735 rebuilds++
1736 return 123
1737 }).pipe(refreshOnSignal)
1738 r.mount(atom)
1739
1740 assert.strictEqual(r.get(atom), 123)
1741 assert.strictEqual(rebuilds, 1)
1742 r.get(atom)
1743 assert.strictEqual(rebuilds, 1)
1744
1745 r.set(signal, 1)
1746 assert.strictEqual(rebuilds, 2)
1747 })
1748
1749 test(`refreshOnSignal uses registry initial values as source state`, () => {
1750 let rebuilds = 0
1751 let value = 0
1752 const signal = Atom.make(0)
1753 const source = Atom.make(() => {
1754 rebuilds++
1755 return value
1756 }).pipe(Atom.keepAlive)
1757 const atom = source.pipe(
1758 Atom.makeRefreshOnSignal(signal),
1759 Atom.keepAlive
1760 )
1761 const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, 10)] })
1762 r.mount(atom)
1763
1764 assert.strictEqual(r.get(atom), 10)
1765 assert.strictEqual(r.get(source), 10)
1766 assert.strictEqual(rebuilds, 1)
1767
1768 value = 11
1769 r.set(signal, 1)
1770
1771 assert.strictEqual(r.get(atom), 11)
1772 assert.strictEqual(r.get(source), 11)
1773 assert.strictEqual(rebuilds, 2)
1774 })
1775
1776 test(`debounce uses registry initial values as source state`, async () => {
1777 const source = Atom.make(0).pipe(Atom.keepAlive)
1778 const atom = source.pipe(
1779 Atom.debounce(100),
1780 Atom.keepAlive
1781 )
1782 const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, 10)] })
1783 r.mount(atom)
1784
1785 assert.strictEqual(r.get(atom), 10)
1786 assert.strictEqual(r.get(source), 10)
1787
1788 r.set(source, 11)
1789 assert.strictEqual(r.get(atom), 10)
1790
1791 await vitest.advanceTimersByTimeAsync(100)
1792
1793 assert.strictEqual(r.get(atom), 11)
1794 assert.strictEqual(r.get(source), 11)
1795 })
1796
1797 test(`withRefresh uses registry initial values as source state`, async () => {
1798 let rebuilds = 0
1799 let value = 0
1800 const source = Atom.make(() => {
1801 rebuilds++
1802 return value
1803 }).pipe(Atom.keepAlive)
1804 const atom = source.pipe(
1805 Atom.withRefresh(100),
1806 Atom.keepAlive
1807 )
1808 const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, 10)] })
1809 r.mount(atom)
1810
1811 assert.strictEqual(r.get(atom), 10)
1812 assert.strictEqual(r.get(source), 10)
1813 assert.strictEqual(rebuilds, 1)
1814
1815 value = 11
1816 await vitest.advanceTimersByTimeAsync(100)
1817
1818 assert.strictEqual(r.get(atom), 11)
1819 assert.strictEqual(r.get(source), 11)
1820 assert.strictEqual(rebuilds, 2)
1821 })
1822
1823 test(`swr refresh is forceful while fresh`, () => {
1824 const r = AtomRegistry.make()
1825 let runs = 0
1826 const atom = Atom.make(Effect.sync(() => ++runs)).pipe(
1827 Atom.swr({ staleTime: 1_000 })
1828 )
1829 const unmount = r.mount(atom)
1830
1831 let result = r.get(atom)
1832 assert(AsyncResult.isSuccess(result))
1833 assert.strictEqual(result.value, 1)
1834 assert.strictEqual(runs, 1)
1835
1836 r.refresh(atom)
1837 result = r.get(atom)
1838 assert(AsyncResult.isSuccess(result))
1839 assert.strictEqual(result.value, 2)
1840 assert.strictEqual(runs, 2)
1841
1842 unmount()
1843 })
1844
1845 test(`swr keeps previous value while stale revalidation runs`, async () => {
1846 const r = AtomRegistry.make()
1847 let runs = 0
1848 const atom = Atom.make(Effect.sync(() => ++runs).pipe(Effect.delay(50))).pipe(
1849 Atom.swr({ staleTime: 100 })
1850 )
1851 const unmount = r.mount(atom)
1852
1853 let result = r.get(atom)
1854 assert(result.waiting)
1855
1856 await vitest.advanceTimersByTimeAsync(50)
1857 result = r.get(atom)
1858 assert(AsyncResult.isSuccess(result))
1859 assert.strictEqual(result.value, 1)
1860 assert.strictEqual(runs, 1)
1861
1862 await vitest.advanceTimersByTimeAsync(101)
1863 r.refresh(atom)
1864 result = r.get(atom)
1865 assert(result.waiting)
1866 assert.deepEqual(AsyncResult.value(result), Option.some(1))
1867 assert.strictEqual(runs, 1)
1868
1869 await vitest.advanceTimersByTimeAsync(50)
1870 result = r.get(atom)
1871 assert(AsyncResult.isSuccess(result))
1872 assert.strictEqual(result.value, 2)
1873 assert.strictEqual(runs, 2)
1874
1875 unmount()
1876 })
1877
1878 test(`swr refresh is forceful after failure with previousSuccess`, () => {
1879 const r = AtomRegistry.make()
1880 let runs = 0
1881 const atom = Atom.fn((i: number) => {
1882 runs++
1883 return i === 1 ? Effect.fail("fail") : Effect.succeed(i)
1884 }).pipe(
1885 Atom.swr({ staleTime: 1_000 })
1886 )
1887 const unmount = r.mount(atom)
1888
1889 r.set(atom, 0)
1890 let result = r.get(atom)
1891 assert(AsyncResult.isSuccess(result))
1892 assert.strictEqual(result.value, 0)
1893 assert.strictEqual(runs, 1)
1894
1895 r.set(atom, 1)
1896 result = r.get(atom)
1897 assert(AsyncResult.isFailure(result))
1898 assert.strictEqual(runs, 2)
1899
1900 r.refresh(atom)
1901 result = r.get(atom)
1902 assert(AsyncResult.isFailure(result))
1903 assert.strictEqual(runs, 3)
1904
1905 r.refresh(atom)
1906 result = r.get(atom)
1907 assert(AsyncResult.isFailure(result))
1908 assert.strictEqual(runs, 4)
1909
1910 unmount()
1911 })
1912
1913 test(`swr refreshes failure without previousSuccess`, () => {
1914 const r = AtomRegistry.make()
1915 let runs = 0
1916 const atom = Atom.make(() => {
1917 runs++
1918 return Effect.fail("fail")
1919 }).pipe(
1920 Atom.swr({ staleTime: 1_000, revalidateOnMount: false })
1921 )
1922 const unmount = r.mount(atom)
1923
1924 let result = r.get(atom)
1925 assert(AsyncResult.isFailure(result))
1926 assert.strictEqual(runs, 1)
1927
1928 r.refresh(atom)
1929 result = r.get(atom)
1930 assert(AsyncResult.isFailure(result))
1931 assert.strictEqual(runs, 2)
1932
1933 unmount()
1934 })
1935
1936 test(`swr revalidateOnMount false skips first read only`, () => {
1937 const r = AtomRegistry.make()
1938 const focusSignal = Atom.make(0)
1939 let focus = 0
1940 const emitFocus = () => r.set(focusSignal, ++focus)
1941 let runs = 0
1942 const atom = Atom.make(() => {
1943 runs++
1944 return Effect.fail("fail")
1945 }).pipe(
1946 Atom.swr({ staleTime: 1_000, revalidateOnMount: false, revalidateOnFocus: true, focusSignal })
1947 )
1948 const unmount = r.mount(atom)
1949
1950 let result = r.get(atom)
1951 assert(AsyncResult.isFailure(result))
1952 assert.strictEqual(runs, 1)
1953
1954 emitFocus()
1955 result = r.get(atom)
1956 assert(AsyncResult.isFailure(result))
1957 assert.strictEqual(runs, 2)
1958
1959 unmount()
1960 })
1961
1962 test(`swr auto revalidates failure with previousSuccess only when stale`, async () => {
1963 const r = AtomRegistry.make()
1964 const focusSignal = Atom.make(0)
1965 let focus = 0
1966 const emitFocus = () => r.set(focusSignal, ++focus)
1967 let runs = 0
1968 const atom = Atom.fn((i: number) => {
1969 runs++
1970 return i === 0 ? Effect.succeed(i) : Effect.fail("fail")
1971 }).pipe(
1972 Atom.swr({ staleTime: 1_000, revalidateOnMount: false, revalidateOnFocus: true, focusSignal })
1973 )
1974 const unmount = r.mount(atom)
1975
1976 r.set(atom, 0)
1977 let result = r.get(atom)
1978 assert(AsyncResult.isSuccess(result))
1979 assert.strictEqual(result.value, 0)
1980 assert.strictEqual(runs, 1)
1981
1982 r.set(atom, 1)
1983 result = r.get(atom)
1984 assert(AsyncResult.isFailure(result))
1985 assert.strictEqual(runs, 2)
1986
1987 emitFocus()
1988 result = r.get(atom)
1989 assert(AsyncResult.isFailure(result))
1990 assert.strictEqual(runs, 2)
1991
1992 await vitest.advanceTimersByTimeAsync(1_001)
1993 emitFocus()
1994 result = r.get(atom)
1995 assert(AsyncResult.isFailure(result))
1996 assert.strictEqual(runs, 3)
1997
1998 unmount()
1999 })
2000
2001 test(`swr does not refresh from initial state`, () => {
2002 const r = AtomRegistry.make()
2003 let runs = 0
2004 const atom = Atom.fn((n: number) => {
2005 runs++
2006 return Effect.succeed(n)
2007 }).pipe(
2008 Atom.swr({ staleTime: 0 })
2009 )
2010 const unmount = r.mount(atom)
2011
2012 let result = r.get(atom)
2013 assert(AsyncResult.isInitial(result))
2014 assert.strictEqual(result.waiting, false)
2015 assert.strictEqual(runs, 0)
2016
2017 r.refresh(atom)
2018 result = r.get(atom)
2019 assert(AsyncResult.isInitial(result))
2020 assert.strictEqual(result.waiting, false)
2021 assert.strictEqual(runs, 0)
2022
2023 unmount()
2024 })
2025
2026 test(`swr refresh is forceful while waiting`, async () => {
2027 const r = AtomRegistry.make()
2028 let runs = 0
2029 const atom = Atom.make(Effect.sync(() => ++runs).pipe(Effect.tap(() => Effect.sleep(50)))).pipe(
2030 Atom.swr({ staleTime: 0 })
2031 )
2032 const unmount = r.mount(atom)
2033
2034 await vitest.advanceTimersByTimeAsync(50)
2035 let result = r.get(atom)
2036 assert(AsyncResult.isSuccess(result))
2037 assert.strictEqual(result.value, 1)
2038 assert.strictEqual(runs, 1)
2039
2040 r.refresh(atom)
2041 result = r.get(atom)
2042 assert(result.waiting)
2043 assert.strictEqual(runs, 2)
2044
2045 r.refresh(atom)
2046 result = r.get(atom)
2047 assert(result.waiting)
2048 assert.strictEqual(runs, 3)
2049
2050 await vitest.advanceTimersByTimeAsync(50)
2051 result = r.get(atom)
2052 assert(AsyncResult.isSuccess(result))
2053 assert.strictEqual(result.value, 3)
2054
2055 unmount()
2056 })
2057
2058 test(`swr delegates refresh to wrapped custom refresh`, () => {
2059 const r = AtomRegistry.make()
2060 let runs = 0
2061 const source = Atom.make(Effect.sync(() => ++runs))
2062 const proxy = Atom.writable<AsyncResult.AsyncResult<number, never>, void>(
2063 (get) => get(source),
2064 () => {
2065 },
2066 (refresh) => refresh(source)
2067 )
2068 const atom = proxy.pipe(Atom.swr({ staleTime: 1_000, revalidateOnMount: false }))
2069 const unmount = r.mount(atom)
2070
2071 let result = r.get(atom)
2072 assert(AsyncResult.isSuccess(result))
2073 assert.strictEqual(result.value, 1)
2074 assert.strictEqual(runs, 1)
2075
2076 r.refresh(atom)
2077 result = r.get(atom)
2078 assert(AsyncResult.isSuccess(result))
2079 assert.strictEqual(result.value, 2)
2080 assert.strictEqual(runs, 2)
2081
2082 unmount()
2083 })
2084
2085 test(`swr revalidates after a stale remount returns`, async () => {
2086 const r = AtomRegistry.make()
2087 let runs = 0
2088 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive)
2089 const atom = base.pipe(Atom.swr({ staleTime: 100, revalidateOnMount: true }))
2090
2091 const unmount1 = r.mount(atom)
2092 let result = r.get(atom)
2093 assert(AsyncResult.isSuccess(result))
2094 assert.strictEqual(result.value, 1)
2095 assert.strictEqual(runs, 1)
2096 unmount1()
2097
2098 await Effect.runPromise(Effect.yieldNow)
2099 await vitest.advanceTimersByTimeAsync(101)
2100
2101 const unmount2 = r.mount(atom)
2102 result = r.get(atom)
2103 assert(AsyncResult.isSuccess(result))
2104 assert.strictEqual(result.value, 1)
2105 assert.strictEqual(runs, 1)
2106
2107 await Effect.runPromise(Effect.yieldNow)
2108 result = r.get(atom)
2109 assert(AsyncResult.isSuccess(result))
2110 assert.strictEqual(result.value, 2)
2111 assert.strictEqual(runs, 2)
2112 unmount2()
2113 })
2114
2115 test(`swr cancels queued revalidation when unmounted`, async () => {
2116 const r = AtomRegistry.make()
2117 let runs = 0
2118 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive)
2119 const atom = base.pipe(Atom.swr({ staleTime: 100 }))
2120 r.get(base)
2121 await vitest.advanceTimersByTimeAsync(101)
2122
2123 const unmount = r.mount(atom)
2124 const result = r.get(atom)
2125 assert(AsyncResult.isSuccess(result))
2126 assert.strictEqual(result.value, 1)
2127 unmount()
2128 await Effect.runPromise(Effect.yieldNow)
2129
2130 assert.strictEqual(runs, 1)
2131 })
2132
2133 test(`swr does not notify subscribers during a nested read`, async () => {
2134 const r = AtomRegistry.make()
2135 const events: Array<string> = []
2136 let runs = 0
2137 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive)
2138 const atom = base.pipe(Atom.swr({ staleTime: 100 }))
2139 r.get(base)
2140 await vitest.advanceTimersByTimeAsync(101)
2141 const unsubscribe = r.subscribe(base, () => events.push("notified"))
2142 const outer = Atom.make(() => {
2143 events.push("read start")
2144 const unmount = r.mount(atom)
2145 const value = r.get(atom)
2146 assert(AsyncResult.isSuccess(value))
2147 assert.strictEqual(value.value, 1)
2148 unmount()
2149 events.push("read end")
2150 return value
2151 })
2152
2153 r.get(outer)
2154 assert.deepStrictEqual(events, ["read start", "read end"])
2155 await Effect.runPromise(Effect.yieldNow)
2156 assert.deepStrictEqual(events, ["read start", "read end"])
2157 assert.strictEqual(runs, 1)
2158 unsubscribe()
2159 })
2160
2161 test(`swr skips queued revalidation when the source becomes fresh`, async () => {
2162 const r = AtomRegistry.make()
2163 let runs = 0
2164 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive)
2165 const atom = base.pipe(Atom.swr({ staleTime: 100 }))
2166 r.get(base)
2167 await vitest.advanceTimersByTimeAsync(101)
2168
2169 const unmount = r.mount(atom)
2170 const result = r.get(atom)
2171 assert(AsyncResult.isSuccess(result))
2172 assert.strictEqual(result.value, 1)
2173 assert.strictEqual(runs, 1)
2174 r.refresh(base)
2175 await Effect.runPromise(Effect.yieldNow)
2176
2177 assert.strictEqual(runs, 2)
2178 unmount()
2179 })
2180
2181 test(`swr does not revalidate on fresh remount when enabled`, async () => {
2182 const r = AtomRegistry.make()
2183 let runs = 0
2184 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive)
2185 const atom = base.pipe(Atom.swr({ staleTime: 10_000, revalidateOnMount: true }))
2186
2187 const unmount1 = r.mount(atom)
2188 let result = r.get(atom)
2189 assert(AsyncResult.isSuccess(result))
2190 assert.strictEqual(result.value, 1)
2191 assert.strictEqual(runs, 1)
2192 unmount1()
2193
2194 await Effect.runPromise(Effect.yieldNow)
2195
2196 const unmount2 = r.mount(atom)
2197 result = r.get(atom)
2198 assert(AsyncResult.isSuccess(result))
2199 assert.strictEqual(result.value, 1)
2200 assert.strictEqual(runs, 1)
2201 unmount2()
2202 })
2203
2204 test(`swr revalidates on focus signal only when stale`, async () => {
2205 const r = AtomRegistry.make()
2206 const focusSignal = Atom.make(0)
2207 let focus = 0
2208 const emitFocus = () => r.set(focusSignal, ++focus)
2209 let runs = 0
2210 const atom = Atom.make(Effect.sync(() => ++runs)).pipe(
2211 Atom.swr({ staleTime: 1_000, revalidateOnFocus: true, focusSignal })
2212 )
2213 const unmount = r.mount(atom)
2214
2215 let result = r.get(atom)
2216 assert(AsyncResult.isSuccess(result))
2217 assert.strictEqual(result.value, 1)
2218 assert.strictEqual(runs, 1)
2219
2220 emitFocus()
2221 result = r.get(atom)
2222 assert(AsyncResult.isSuccess(result))
2223 assert.strictEqual(result.value, 1)
2224 assert.strictEqual(runs, 1)
2225
2226 await vitest.advanceTimersByTimeAsync(1_001)
2227 emitFocus()
2228 result = r.get(atom)
2229 assert(AsyncResult.isSuccess(result))
2230 assert.strictEqual(result.value, 2)
2231 assert.strictEqual(runs, 2)
2232
2233 unmount()
2234 })
2235
2236 test(`swr can force refresh on focus signal`, () => {
2237 const r = AtomRegistry.make()
2238 const focusSignal = Atom.make(0)
2239 let focus = 0
2240 const emitFocus = () => r.set(focusSignal, ++focus)
2241 let runs = 0
2242 const atom = Atom.make(Effect.sync(() => ++runs)).pipe(
2243 Atom.swr({ staleTime: 1_000, revalidateOnFocus: "always", focusSignal })
2244 )
2245 const unmount = r.mount(atom)
2246
2247 let result = r.get(atom)
2248 assert(AsyncResult.isSuccess(result))
2249 assert.strictEqual(result.value, 1)
2250 assert.strictEqual(runs, 1)
2251
2252 emitFocus()
2253 result = r.get(atom)
2254 assert(AsyncResult.isSuccess(result))
2255 assert.strictEqual(result.value, 2)
2256 assert.strictEqual(runs, 2)
2257
2258 unmount()
2259 })
2260
2261 test(`swr treats value as stale at exact staleTime boundary`, async () => {
2262 const r = AtomRegistry.make()
2263 const focusSignal = Atom.make(0)
2264 let focus = 0
2265 const emitFocus = () => r.set(focusSignal, ++focus)
2266 let runs = 0
2267 const atom = Atom.make(Effect.sync(() => ++runs)).pipe(
2268 Atom.swr({ staleTime: 1_000, revalidateOnFocus: true, focusSignal })
2269 )
2270 const unmount = r.mount(atom)
2271
2272 let result = r.get(atom)
2273 assert(AsyncResult.isSuccess(result))
2274 assert.strictEqual(result.value, 1)
2275 assert.strictEqual(runs, 1)
2276
2277 await vitest.advanceTimersByTimeAsync(1_000)
2278 emitFocus()
2279 result = r.get(atom)
2280 assert(AsyncResult.isSuccess(result))
2281 assert.strictEqual(result.value, 2)
2282 assert.strictEqual(runs, 2)
2283
2284 unmount()
2285 })
2286
2287 test(`swr does not refresh on focus signal when disabled or omitted`, () => {
2288 {
2289 const r = AtomRegistry.make()
2290 const focusSignal = Atom.make(0)
2291 let focus = 0
2292 const emitFocus = () => r.set(focusSignal, ++focus)
2293 let runs = 0
2294 const atom = Atom.make(Effect.sync(() => ++runs)).pipe(
2295 Atom.swr({ staleTime: 10_000, revalidateOnFocus: false, focusSignal })
2296 )
2297 const unmount = r.mount(atom)
2298
2299 let result = r.get(atom)
2300 assert(AsyncResult.isSuccess(result))
2301 assert.strictEqual(result.value, 1)
2302 assert.strictEqual(runs, 1)
2303
2304 emitFocus()
2305 result = r.get(atom)
2306 assert(AsyncResult.isSuccess(result))
2307 assert.strictEqual(result.value, 1)
2308 assert.strictEqual(runs, 1)
2309
2310 unmount()
2311 }
2312
2313 {
2314 const r = AtomRegistry.make()
2315 const focusSignal = Atom.make(0)
2316 let focus = 0
2317 const emitFocus = () => r.set(focusSignal, ++focus)
2318 let runs = 0
2319 const atom = Atom.make(Effect.sync(() => ++runs)).pipe(
2320 Atom.swr({ staleTime: 10_000, focusSignal })
2321 )
2322 const unmount = r.mount(atom)
2323
2324 let result = r.get(atom)
2325 assert(AsyncResult.isSuccess(result))
2326 assert.strictEqual(result.value, 1)
2327 assert.strictEqual(runs, 1)
2328
2329 emitFocus()
2330 result = r.get(atom)
2331 assert(AsyncResult.isSuccess(result))
2332 assert.strictEqual(result.value, 1)
2333 assert.strictEqual(runs, 1)
2334
2335 unmount()
2336 }
2337 })
2338
2339 test(`swr does not revalidate on stale remount when disabled`, async () => {
2340 const r = AtomRegistry.make()
2341 let runs = 0
2342 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive)
2343 const atom = base.pipe(Atom.swr({ staleTime: 100, revalidateOnMount: false }))
2344
2345 const unmount1 = r.mount(atom)
2346 let result = r.get(atom)
2347 assert(AsyncResult.isSuccess(result))
2348 assert.strictEqual(result.value, 1)
2349 assert.strictEqual(runs, 1)
2350 unmount1()
2351
2352 await Effect.runPromise(Effect.yieldNow)
2353 await vitest.advanceTimersByTimeAsync(101)
2354
2355 const unmount2 = r.mount(atom)
2356 result = r.get(atom)
2357 assert(AsyncResult.isSuccess(result))
2358 assert.strictEqual(result.value, 1)
2359 assert.strictEqual(runs, 1)
2360 unmount2()
2361 })
2362
2363 test(`swr composes with signal driven refresh wrappers`, async () => {
2364 const r = AtomRegistry.make()
2365 let runs = 0
2366 const signal = Atom.make(0)
2367 const atom = Atom.make(Effect.sync(() => ++runs)).pipe(
2368 Atom.swr({ staleTime: 1_000 }),
2369 Atom.makeRefreshOnSignal(signal)
2370 )
2371 const unmount = r.mount(atom)
2372
2373 let result = r.get(atom)
2374 assert(AsyncResult.isSuccess(result))
2375 assert.strictEqual(result.value, 1)
2376 assert.strictEqual(runs, 1)
2377
2378 r.set(signal, 1)
2379 result = r.get(atom)
2380 assert(AsyncResult.isSuccess(result))
2381 assert.strictEqual(result.value, 2)
2382 assert.strictEqual(runs, 2)
2383
2384 r.set(signal, 2)
2385 result = r.get(atom)
2386 assert(AsyncResult.isSuccess(result))
2387 assert.strictEqual(result.value, 3)
2388 assert.strictEqual(runs, 3)
2389
2390 unmount()
2391 })
2392
2393 test(`swr preserves writable set semantics`, () => {
2394 const r = AtomRegistry.make()
2395 let writes = 0
2396 const source = Atom.writable<AsyncResult.AsyncResult<number, never>, number>(
2397 (get) => Option.getOrElse(get.self<AsyncResult.AsyncResult<number, never>>(), () => AsyncResult.success(0)),
2398 (ctx, value) => {
2399 writes++
2400 ctx.setSelf(AsyncResult.success(value))
2401 }
2402 )
2403 const atom = source.pipe(Atom.swr({ staleTime: 1_000 }))
2404 const unmount = r.mount(atom)
2405
2406 let result = r.get(atom)
2407 assert(AsyncResult.isSuccess(result))
2408 assert.strictEqual(result.value, 0)
2409
2410 r.set(atom, 1)
2411 result = r.get(atom)
2412 assert(AsyncResult.isSuccess(result))
2413 assert.strictEqual(result.value, 1)
2414 assert.strictEqual(writes, 1)
2415
2416 const sourceResult = r.get(source)
2417 assert(AsyncResult.isSuccess(sourceResult))
2418 assert.strictEqual(sourceResult.value, 1)
2419
2420 unmount()
2421 })
2422
2423 test(`swr uses registry initial values as source state`, () => {
2424 let runs = 0
2425 const source = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive)
2426 const atom = source.pipe(
2427 Atom.swr({ staleTime: 1_000, revalidateOnMount: false }),
2428 Atom.keepAlive
2429 )
2430 const initial = AsyncResult.success(10)
2431 const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, initial)] })
2432 r.mount(atom)
2433
2434 assert.deepStrictEqual(r.get(atom), initial)
2435 assert.deepStrictEqual(r.get(source), initial)
2436 assert.strictEqual(runs, 1)
2437
2438 r.refresh(atom)
2439
2440 const result = r.get(atom)
2441 assert(AsyncResult.isSuccess(result))
2442 assert.strictEqual(result.value, 2)
2443 assert.strictEqual(runs, 2)
2444 })
2445
2446 // it("dehydrate", async () => {
2447 // const r = AtomRegistry.make()
2448 // const notSerializable = Atom.make(0)
2449 // r.mount(notSerializable)
2450 //
2451 // const basicSerializable = Atom.make(0).pipe(Atom.serializable({
2452 // key: "basicSerializable",
2453 // schema: Schema.Number
2454 // }))
2455 // r.mount(basicSerializable)
2456 //
2457 // const errored = Atom.make(Effect.fail("error")).pipe(
2458 // Atom.serializable({
2459 // key: "errored",
2460 // schema: AsyncResult.Schema({
2461 // error: Schema.String
2462 // })
2463 // })
2464 // )
2465 // r.mount(errored)
2466 //
2467 // const success = Atom.make(Effect.succeed(123)).pipe(Atom.serializable({
2468 // key: "success",
2469 // schema: AsyncResult.Schema({
2470 // success: Schema.Number
2471 // })
2472 // }))
2473 // r.mount(success)
2474 //
2475 // const { promise, resolve } = Promise.withResolvers<number>()
2476 //
2477 // const pending = Atom.make(Effect.promise(() => promise)).pipe(Atom.serializable({
2478 // key: "pending",
2479 // schema: AsyncResult.Schema({
2480 // success: Schema.Number
2481 // })
2482 // }))
2483 // r.mount(pending)
2484 //
2485 // const state = Hydration.toValues(Hydration.dehydrate(r, {
2486 // encodeInitialAs: "promise"
2487 // }))
2488 // expect(state.map((r) => Struct.omit(r, "dehydratedAt", "resultPromise"))).toMatchInlineSnapshot(`
2489 // [
2490 // {
2491 // "key": "basicSerializable",
2492 // "value": 0,
2493 // "~@effect-atom/atom/DehydratedAtom": true,
2494 // },
2495 // {
2496 // "key": "errored",
2497 // "value": {
2498 // "_tag": "Failure",
2499 // "cause": {
2500 // "_tag": "Fail",
2501 // "error": "error",
2502 // },
2503 // "previousSuccess": {
2504 // "_tag": "None",
2505 // },
2506 // "waiting": false,
2507 // },
2508 // "~@effect-atom/atom/DehydratedAtom": true,
2509 // },
2510 // {
2511 // "key": "success",
2512 // "value": {
2513 // "_tag": "Success",
2514 // "timestamp": ${Date.now()},
2515 // "value": 123,
2516 // "waiting": false,
2517 // },
2518 // "~@effect-atom/atom/DehydratedAtom": true,
2519 // },
2520 // {
2521 // "key": "pending",
2522 // "value": {
2523 // "_tag": "Initial",
2524 // "waiting": true,
2525 // },
2526 // "~@effect-atom/atom/DehydratedAtom": true,
2527 // },
2528 // ]
2529 // `)
2530 //
2531 // expect(state.find((r) => r.key === "pending")?.resultPromise).instanceOf(Promise)
2532 //
2533 // const r2 = AtomRegistry.make()
2534 // Hydration.hydrate(r2, state)
2535 //
2536 // expect(r2.get(notSerializable)).toEqual(0)
2537 // expect(r2.get(basicSerializable)).toEqual(0)
2538 // expect(r2.get(errored)).toEqual(AsyncResult.failure(Cause.fail("error")))
2539 // expect(r2.get(success)).toEqual(AsyncResult.success(123))
2540 // expect(r2.get(pending)).toEqual(AsyncResult.initial(true))
2541 //
2542 // resolve(123)
2543 // await expect(state.find((r) => r.key === "pending")?.resultPromise).resolves.toEqual({
2544 // "_tag": "Success",
2545 // "timestamp": expect.any(Number),
2546 // "value": 123,
2547 // "waiting": false
2548 // })
2549 // })
2550
2551 describe("optimistic", () => {
2552 it("non-Result", async () => {
2553 const latch = Latch.makeUnsafe()
2554 const r = AtomRegistry.make()
2555 let i = 0
2556 const atom = Atom.make(() => i)
2557 const optimisticAtom = atom.pipe(Atom.optimistic)
2558 const fn = optimisticAtom.pipe(
2559 Atom.optimisticFn({
2560 reducer: (_current, update: number) => update,
2561 fn: Atom.fn(Effect.fnUntraced(function*() {
2562 yield* latch.await
2563 }))
2564 }),
2565 Atom.keepAlive
2566 )
2567
2568 expect(r.get(atom)).toEqual(0)
2569 expect(r.get(optimisticAtom)).toEqual(0)
2570 r.set(fn, 1)
2571 i = 2
2572
2573 // optimistic phase: the optimistic value is set, but the true value is not
2574 expect(r.get(atom)).toEqual(0)
2575 expect(r.get(optimisticAtom)).toEqual(1)
2576
2577 latch.openUnsafe()
2578 await Effect.runPromise(Effect.yieldNow)
2579
2580 // commit phase: a refresh is triggered, the authoritative value is used
2581 expect(r.get(atom)).toEqual(2)
2582 expect(r.get(optimisticAtom)).toEqual(2)
2583 })
2584
2585 it("Result", async () => {
2586 const runtime = Atom.runtime(Layer.empty)
2587 const latch = Latch.makeUnsafe()
2588 const r = AtomRegistry.make()
2589 let i = 0
2590 const atom = Atom.make(Effect.sync(() => {
2591 return i
2592 }))
2593 const optimisticAtom = atom.pipe(
2594 Atom.optimistic
2595 )
2596 const fn = optimisticAtom.pipe(
2597 Atom.optimisticFn({
2598 reducer: (_current, update: number) => AsyncResult.success(update),
2599 fn: runtime.fn(Effect.fnUntraced(function*() {
2600 yield* latch.await
2601 }))
2602 })
2603 )
2604
2605 r.mount(optimisticAtom)
2606 r.mount(fn)
2607
2608 expect(r.get(atom)).toEqual(AsyncResult.success(0))
2609 expect(r.get(optimisticAtom)).toEqual(AsyncResult.success(0))
2610 r.set(fn, 1)
2611 i = 2
2612
2613 // optimistic phase: the optimistic value is set, but the true value is not
2614 expect(r.get(atom)).toEqual(AsyncResult.success(0))
2615 expect(r.get(optimisticAtom)).toEqual(AsyncResult.success(1, { waiting: true }))
2616
2617 latch.openUnsafe()
2618 await Effect.runPromise(Effect.yieldNow)
2619
2620 // commit phase: a refresh is triggered, the authoritative value is used
2621 expect(r.get(atom)).toEqual(AsyncResult.success(2))
2622 expect(r.get(optimisticAtom)).toEqual(AsyncResult.success(2))
2623 })
2624
2625 it("failures", async () => {
2626 const latch = Latch.makeUnsafe()
2627 const r = AtomRegistry.make()
2628 const i = 0
2629 let rebuilds = 0
2630 const atom = Atom.make(() => {
2631 rebuilds++
2632 return i
2633 })
2634 const optimisticAtom = atom.pipe(
2635 Atom.optimistic
2636 )
2637 const fn = optimisticAtom.pipe(
2638 Atom.optimisticFn({
2639 reducer: (_, value) => value,
2640 fn: Atom.fn<number>()(Effect.fnUntraced(function*() {
2641 yield* latch.await
2642 return yield* Effect.fail("error")
2643 }))
2644 })
2645 )
2646
2647 r.mount(fn)
2648 r.mount(optimisticAtom)
2649
2650 expect(r.get(atom)).toEqual(0)
2651 expect(r.get(optimisticAtom)).toEqual(0)
2652 r.set(fn, 1)
2653
2654 // optimistic phase: the optimistic value is set, but the true value is not
2655 expect(r.get(atom)).toEqual(0)
2656 expect(r.get(optimisticAtom)).toEqual(1)
2657
2658 latch.openUnsafe()
2659 await Effect.runPromise(Effect.yieldNow)
2660
2661 // commit phase: the optimistic value is reset to the true value
2662 expect(r.get(atom)).toEqual(0)
2663 expect(r.get(optimisticAtom)).toEqual(0)
2664 expect(rebuilds).toEqual(1)
2665 })
2666
2667 it("sync fn", async () => {
2668 const r = AtomRegistry.make()
2669 let i = 0
2670 const atom = Atom.make(() => i)
2671 const optimisticAtom = atom.pipe(Atom.optimistic, Atom.keepAlive)
2672 const fn = optimisticAtom.pipe(
2673 Atom.optimisticFn({
2674 reducer: (_current, update) => update,
2675 fn: Atom.fn<number>()(() => {
2676 i = 2
2677 return Effect.void
2678 })
2679 })
2680 )
2681
2682 expect(r.get(atom)).toEqual(0)
2683 expect(r.get(optimisticAtom)).toEqual(0)
2684 r.set(fn, 1)
2685
2686 expect(r.get(atom)).toEqual(2)
2687 expect(r.get(optimisticAtom)).toEqual(2)
2688 })
2689
2690 it("intermediate updates", async () => {
2691 const latch = Latch.makeUnsafe()
2692 const r = AtomRegistry.make()
2693 let i = 0
2694 const atom = Atom.make(Effect.sync(() => i))
2695 const optimisticAtom = atom.pipe(
2696 Atom.optimistic
2697 )
2698 const fn = optimisticAtom.pipe(
2699 Atom.optimisticFn({
2700 reducer: (_current, update: number) => AsyncResult.success(update),
2701 fn: (set) =>
2702 Atom.fn(Effect.fnUntraced(function*() {
2703 set(AsyncResult.success(123))
2704 yield* latch.await
2705 }))
2706 }),
2707 Atom.keepAlive
2708 )
2709
2710 expect(r.get(atom)).toEqual(AsyncResult.success(0))
2711 assert.deepStrictEqual(r.get(optimisticAtom), AsyncResult.success(0))
2712 r.set(fn, 1)
2713 i = 2
2714
2715 // optimistic phase: the intermediate value is set, but the true value is
2716 // not
2717 assert.deepStrictEqual(r.get(atom), AsyncResult.success(0))
2718 assert.deepStrictEqual(r.get(optimisticAtom), AsyncResult.success(123, { waiting: true }))
2719
2720 latch.openUnsafe()
2721 await Effect.runPromise(Effect.yieldNow)
2722
2723 // commit phase: a refresh is triggered, the authoritative value is used
2724 assert.deepStrictEqual(r.get(atom), AsyncResult.success(2))
2725 assert.deepStrictEqual(r.get(optimisticAtom), AsyncResult.success(2))
2726 })
2727 })
2728
2729 describe("Reactivity", () => {
2730 it.effect("cleans up queries with duplicate keys", () =>
2731 Effect.gen(function*() {
2732 const reactivity = yield* Reactivity.make
2733 const results = yield* reactivity.query(["todos", "todos"], Effect.succeed(42))
2734 assert.strictEqual(yield* Queue.take(results), 42)
2735 }).pipe(Effect.scoped))
2736
2737 it("does not broadcast mutations across registries", () => {
2738 let reads = 0
2739 const query = Atom.make(() => ++reads).pipe(
2740 Atom.withReactivity(["counter"]),
2741 Atom.keepAlive
2742 )
2743 const runtime = Atom.runtime(Layer.empty)
2744 const mutation = runtime.fn(
2745 Effect.fn(function*() {
2746 }),
2747 { reactivityKeys: ["counter"] }
2748 )
2749 const registryA = AtomRegistry.make()
2750 const registryB = AtomRegistry.make()
2751
2752 expect(registryA.get(query)).toEqual(1)
2753 expect(registryB.get(query)).toEqual(2)
2754
2755 registryA.set(mutation, void 0)
2756
2757 expect(reads).toEqual(3)
2758 expect(registryA.get(query)).toEqual(3)
2759 expect(registryB.get(query)).toEqual(2)
2760
2761 registryA.dispose()
2762 registryB.dispose()
2763 })
2764
2765 it("rebuilds on mutation", async () => {
2766 const r = AtomRegistry.make()
2767 let rebuilds = 0
2768 const atom = Atom.make(() => rebuilds++).pipe(
2769 Atom.withReactivity(["counter"]),
2770 Atom.keepAlive
2771 )
2772 const fn = counterRuntime.fn(
2773 Effect.fn(function*() {
2774 }),
2775 { reactivityKeys: ["counter"] }
2776 )
2777 assert.strictEqual(r.get(atom), 0)
2778 r.set(fn, void 0)
2779 assert.strictEqual(r.get(atom), 1)
2780 r.set(fn, void 0)
2781 r.set(fn, void 0)
2782 assert.strictEqual(r.get(atom), 3)
2783 })
2784
2785 it("rebuilds on mutation with a registry initial value", async () => {
2786 let rebuilds = 0
2787 let value = 0
2788 const atom = Atom.make(() => {
2789 rebuilds++
2790 return value
2791 }).pipe(
2792 Atom.withReactivity(["counter"]),
2793 Atom.keepAlive
2794 )
2795 const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, 10)] })
2796 const fn = counterRuntime.fn(
2797 Effect.fn(function*() {
2798 }),
2799 { reactivityKeys: ["counter"] }
2800 )
2801 r.mount(atom)
2802
2803 assert.strictEqual(r.get(atom), 10)
2804 assert.strictEqual(rebuilds, 1)
2805
2806 value = 11
2807 r.set(fn, void 0)
2808
2809 assert.strictEqual(r.get(atom), 11)
2810 assert.strictEqual(rebuilds, 2)
2811 })
2812
2813 it("rebuilds on mutation with a hydrated value", async () => {
2814 let rebuilds = 0
2815 let value = 0
2816 const atom = Atom.make(() => {
2817 rebuilds++
2818 return value
2819 }).pipe(
2820 Atom.withReactivity(["counter"]),
2821 Atom.serializable({ key: "hydrated-counter", schema: Schema.Number }),
2822 Atom.keepAlive
2823 )
2824 const r = AtomRegistry.make()
2825 const fn = counterRuntime.fn(
2826 Effect.fn(function*() {
2827 }),
2828 { reactivityKeys: ["counter"] }
2829 )
2830 const dehydratedState: Array<Hydration.DehydratedAtomValue> = [{
2831 "~effect/reactivity/Hydration/DehydratedAtom": true,
2832 key: "hydrated-counter",
2833 value: 10,
2834 dehydratedAt: 0
2835 }]
2836 Hydration.hydrate(r, dehydratedState)
2837 r.mount(atom)
2838
2839 assert.strictEqual(r.get(atom), 10)
2840 assert.strictEqual(rebuilds, 1)
2841
2842 value = 11
2843 r.set(fn, void 0)
2844
2845 assert.strictEqual(r.get(atom), 11)
2846 assert.strictEqual(rebuilds, 2)
2847 })
2848 })
2849
2850 it("Atom.Interrupt", async () => {
2851 const r = AtomRegistry.make()
2852 const atom = Atom.fn(() => Effect.never)
2853 r.mount(atom)
2854 expect(r.get(atom)).toEqual(AsyncResult.initial())
2855 expect(r.get(atom).waiting).toBeFalsy()
2856 r.set(atom, void 0)
2857 expect(r.get(atom)).toEqual(AsyncResult.initial(true))
2858 expect(r.get(atom).waiting).toBeTruthy()
2859
2860 r.set(atom, Atom.Interrupt)
2861 await Effect.runPromise(Effect.yieldNow)
2862 const result = r.get(atom)
2863 expect(AsyncResult.isInterrupted(result)).toBeTruthy()
2864 })
2865
2866 it("writable derived clears waiting after refresh", async () => {
2867 let count = 0
2868 const base = Atom.make(Effect.sync(() => ++count).pipe(Effect.delay(100))).pipe(
2869 Atom.withLabel("base")
2870 )
2871 const derived = Atom.writable(
2872 (get) => get(base),
2873 () => {},
2874 (refresh) => refresh(base)
2875 ).pipe(
2876 Atom.withLabel("derived")
2877 )
2878 const r = AtomRegistry.make()
2879
2880 const unmount1 = r.mount(derived)
2881 await vitest.advanceTimersByTimeAsync(100)
2882 let result = r.get(derived)
2883 assert(AsyncResult.isSuccess(result))
2884 expect(result.value).toEqual(1)
2885 expect(result.waiting).toEqual(false)
2886 unmount1()
2887
2888 r.refresh(derived)
2889 const unmount2 = r.mount(derived)
2890 await vitest.advanceTimersByTimeAsync(100)
2891
2892 result = r.get(derived)
2893 expect(result.waiting).toEqual(false)
2894 assert(AsyncResult.isSuccess(result))
2895 expect(result.value).toEqual(2)
2896
2897 unmount2()
2898 })
2899
2900 it("get.result suspendOnWaiting", async () => {
2901 const r = AtomRegistry.make()
2902
2903 const inner = Atom.make(Effect.succeed(1).pipe(Effect.delay(50)))
2904 const outer = Atom.make((get) => get.result(inner, { suspendOnWaiting: true }))
2905
2906 r.mount(outer)
2907
2908 let result = r.get(outer)
2909 assert(result.waiting)
2910
2911 await vitest.advanceTimersByTimeAsync(50)
2912
2913 result = r.get(outer)
2914 assert(AsyncResult.isSuccess(result))
2915 assert.strictEqual(result.value, 1)
2916 })
2917
2918 it("fn get.result suspendOnWaiting", async () => {
2919 const r = AtomRegistry.make()
2920 let runs = 0
2921
2922 const inner = Atom.fn((n: number) => {
2923 runs++
2924 return Effect.succeed(n * 2).pipe(Effect.delay(50))
2925 })
2926
2927 const outer = Atom.fn(
2928 Effect.fn(function*(_: void, get: Atom.FnContext) {
2929 get.set(inner, 1)
2930 const a = yield* get.result(inner, { suspendOnWaiting: true })
2931
2932 get.set(inner, 2)
2933 const b = yield* get.result(inner, { suspendOnWaiting: true })
2934
2935 return { a, b }
2936 })
2937 )
2938
2939 r.mount(outer)
2940 r.set(outer, void 0)
2941
2942 await vitest.advanceTimersByTimeAsync(100)
2943
2944 const result = r.get(outer)
2945 assert(AsyncResult.isSuccess(result))
2946 assert.strictEqual(result.value.a, 2)
2947 assert.strictEqual(result.value.b, 4)
2948 assert.strictEqual(runs, 2)
2949 })
2950
2951 describe("kvs", () => {
2952 it("memoizes defaultValue while loading empty storage", async () => {
2953 let calls = 0
2954 const storage = new Map<string, string>()
2955
2956 const DelayedKVS = Layer.succeed(
2957 KeyValueStore.KeyValueStore,
2958 KeyValueStore.makeStringOnly({
2959 get: (key) =>
2960 Effect.gen(function*() {
2961 yield* Effect.sleep(20)
2962 return storage.get(key)
2963 }),
2964 set: (key, value) =>
2965 Effect.sync(() => {
2966 storage.set(key, value)
2967 }),
2968 remove: (key) =>
2969 Effect.sync(() => {
2970 storage.delete(key)
2971 }),
2972 clear: Effect.sync(() => storage.clear()),
2973 size: Effect.sync(() => storage.size)
2974 })
2975 )
2976
2977 const kvsRuntime = Atom.runtime(DelayedKVS)
2978 const atom = Atom.kvs({
2979 runtime: kvsRuntime,
2980 key: "default-value-key",
2981 schema: Schema.Number,
2982 defaultValue: () => {
2983 calls++
2984 return 0
2985 }
2986 })
2987
2988 const r = AtomRegistry.make()
2989 r.mount(atom)
2990
2991 expect(r.get(atom)).toEqual(0)
2992 expect(calls).toEqual(1)
2993
2994 await vitest.advanceTimersByTimeAsync(50)
2995
2996 expect(r.get(atom)).toEqual(0)
2997 expect(calls).toEqual(1)
2998 })
2999
3000 it("preserves existing value after async load completes", async () => {
3001 vitest.useRealTimers()
3002 // Create an in-memory store with a pre-existing value
3003 const storage = new Map<string, string>()
3004 storage.set("test-key", JSON.stringify(42))
3005
3006 // Create a delayed KeyValueStore to simulate async loading
3007 // Use KeyValueStore.make to get proper forSchema support
3008 const DelayedKVS = Layer.succeed(
3009 KeyValueStore.KeyValueStore,
3010 KeyValueStore.makeStringOnly({
3011 get: (key) =>
3012 Effect.gen(function*() {
3013 yield* Effect.sleep(20) // Short delay to create Initial state window
3014 return storage.get(key)
3015 }),
3016 set: (key, value) =>
3017 Effect.sync(() => {
3018 storage.set(key, value)
3019 }),
3020 remove: (key) =>
3021 Effect.sync(() => {
3022 storage.delete(key)
3023 }),
3024 clear: Effect.sync(() => storage.clear()),
3025 size: Effect.sync(() => storage.size)
3026 })
3027 )
3028
3029 const kvsRuntime = Atom.runtime(DelayedKVS)
3030 const atom = Atom.kvs({
3031 runtime: kvsRuntime,
3032 key: "test-key",
3033 schema: Schema.Number,
3034 defaultValue: () => 0
3035 })
3036
3037 const r = AtomRegistry.make()
3038 r.mount(atom)
3039
3040 // First read during Initial state returns default
3041 const value = r.get(atom)
3042 expect(value).toEqual(0)
3043
3044 // Wait for async load AND any set effects to complete
3045 await new Promise((resolve) => setTimeout(resolve, 50))
3046
3047 // THE KEY ASSERTION: After load completes, storage should still have original value.
3048 // The bug was that the default (0) would be written during Initial state,
3049 // corrupting the storage before the async load could read it.
3050 expect(storage.get("test-key")).toEqual(JSON.stringify(42))
3051 })
3052
3053 it("async mode", async () => {
3054 vitest.useRealTimers()
3055 // Create an in-memory store with a pre-existing value
3056 const storage = new Map<string, string>()
3057 storage.set("test-key", JSON.stringify(42))
3058
3059 // Create a delayed KeyValueStore to simulate async loading
3060 // Use KeyValueStore.make to get proper forSchema support
3061 const DelayedKVS = Layer.succeed(
3062 KeyValueStore.KeyValueStore,
3063 KeyValueStore.makeStringOnly({
3064 get: (key) =>
3065 Effect.gen(function*() {
3066 yield* Effect.sleep(20) // Short delay to create Initial state window
3067 return storage.get(key)
3068 }),
3069 set: (key, value) =>
3070 Effect.sync(() => {
3071 storage.set(key, value)
3072 }),
3073 remove: (key) =>
3074 Effect.sync(() => {
3075 storage.delete(key)
3076 }),
3077 clear: Effect.sync(() => storage.clear()),
3078 size: Effect.sync(() => storage.size)
3079 })
3080 )
3081
3082 const kvsRuntime = Atom.runtime(DelayedKVS)
3083 const atom = Atom.kvs({
3084 mode: "async",
3085 runtime: kvsRuntime,
3086 key: "test-key",
3087 schema: Schema.Number,
3088 defaultValue: () => 0
3089 })
3090
3091 const r = AtomRegistry.make()
3092 r.mount(atom)
3093
3094 expect(r.get(atom)).toEqual(AsyncResult.initial(true))
3095
3096 await new Promise((resolve) => setTimeout(resolve, 50))
3097
3098 const result = r.get(atom)
3099 assert(AsyncResult.isSuccess(result))
3100 expect(result.value).toEqual(42)
3101 expect(result.waiting).toEqual(false)
3102 expect(storage.get("test-key")).toEqual(JSON.stringify(42))
3103
3104 r.set(atom, 24)
3105
3106 const updated = r.get(atom)
3107 assert(AsyncResult.isSuccess(updated))
3108 expect(updated.value).toEqual(24)
3109 })
3110 })
3111})
3112
3113interface BuildCounter {
3114 readonly get: Effect.Effect<number>
3115 readonly inc: Effect.Effect<void>
3116}
3117const BuildCounter = Context.Service<BuildCounter>("BuildCounter")
3118const BuildCounterLayer = Layer.sync(BuildCounter, () => {
3119 let count = 0
3120 return BuildCounter.of({
3121 get: Effect.sync(() => count),
3122 inc: Effect.sync(() => {
3123 count++
3124 })
3125 })
3126})
3127
3128interface Counter {
3129 readonly get: Effect.Effect<number>
3130 readonly inc: Effect.Effect<void>
3131}
3132const Counter = Context.Service<Counter>("Counter")
3133const CounterLayer = Layer.effect(
3134 Counter,
3135 Effect.gen(function*() {
3136 const buildCounter = yield* BuildCounter
3137 yield* buildCounter.inc
3138 let count = 1
3139 return Counter.of({
3140 get: Effect.sync(() => count),
3141 inc: Effect.sync(() => {
3142 count++
3143 })
3144 })
3145 })
3146).pipe(
3147 Layer.provide(BuildCounterLayer)
3148)
3149
3150const CounterTest = Layer.effect(
3151 Counter,
3152 Effect.gen(function*() {
3153 const buildCounter = yield* BuildCounter
3154 yield* buildCounter.inc
3155 let count = 10
3156 return Counter.of({
3157 get: Effect.sync(() => count),
3158 inc: Effect.sync(() => {
3159 count++
3160 })
3161 })
3162 })
3163).pipe(
3164 Layer.provide(BuildCounterLayer)
3165)
3166
3167interface Multiplier {
3168 readonly times: (n: number) => Effect.Effect<number>
3169}
3170const Multiplier = Context.Service<Multiplier>("Multiplier")
3171const MultiplierLayer = Layer.effect(
3172 Multiplier,
3173 Effect.gen(function*() {
3174 const counter = yield* Counter
3175 yield* AtomRegistry.AtomRegistry // test that we can access the registry
3176 return Multiplier.of({
3177 times: (n) => Effect.map(counter.get, (_) => _ * n)
3178 })
3179 })
3180).pipe(
3181 Layer.provideMerge(CounterLayer)
3182)
3183
3184const buildCounterRuntime = Atom.runtime(BuildCounterLayer)
3185const counterRuntime = Atom.runtime(CounterLayer)
3186const multiplierRuntime = Atom.runtime(MultiplierLayer)