import { addEqualityTesters, afterEach, assert, beforeEach, describe, expect, it, test, vitest } from "@effect/vitest" import { Array as Arr, Cause, Context, Effect, Hash, Latch, Layer, Option, Result, Schema, Stream, SubscriptionRef } from "effect" import { TestClock } from "effect/testing" import { KeyValueStore } from "effect/unstable/persistence" import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity" declare const global: any addEqualityTesters() describe.sequential("Atom", () => { beforeEach(async () => { vitest.useFakeTimers({ toFake: [ "Date", "hrtime", "setTimeout", "clearTimeout", "setInterval", "clearInterval", "performance" ] }) await Effect.runPromise(Effect.yieldNow) }) afterEach(() => { vitest.useRealTimers() }) it("get/set", () => { const counter = Atom.make(0) const r = AtomRegistry.make() expect(r.get(counter)).toEqual(0) r.set(counter, 1) expect(r.get(counter)).toEqual(1) }) it("keepAlive false", async () => { const counter = Atom.make(0) const r = AtomRegistry.make() r.set(counter, 1) expect(r.get(counter)).toEqual(1) await Effect.runPromise(Effect.yieldNow) expect(r.get(counter)).toEqual(0) }) it("keepAlive true", async () => { const counter = Atom.make(0).pipe( Atom.keepAlive ) const r = AtomRegistry.make() r.set(counter, 1) expect(r.get(counter)).toEqual(1) await new Promise((resolve) => resolve(null)) expect(r.get(counter)).toEqual(1) }) it("subscribe", async () => { const counter = Atom.make(0) const r = AtomRegistry.make() let count = 0 const cancel = r.subscribe(counter, (_) => { count = _ }) r.set(counter, 1) expect(count).toEqual(1) await Effect.runPromise(Effect.yieldNow) expect(r.get(counter)).toEqual(1) cancel() await Effect.runPromise(Effect.yieldNow) expect(r.get(counter)).toEqual(0) }) it("subscribe does not skip listeners when unsubscribing during notify", () => { const counter = Atom.make(0) const r = AtomRegistry.make() let first = 0 let second = 0 let cancelFirst = () => { } cancelFirst = r.subscribe(counter, () => { first++ cancelFirst() }) r.subscribe(counter, () => { second++ }) r.set(counter, 1) expect(first).toEqual(1) expect(second).toEqual(1) }) it("searchParam with schema reads initial query value", () => { const previousWindow = (globalThis as any).window const r = AtomRegistry.make() Object.defineProperty(globalThis, "window", { value: { location: { pathname: "/", search: "?page=6" }, history: { pushState: () => { } }, addEventListener: () => { }, removeEventListener: () => { } }, configurable: true, writable: true }) try { const page = Atom.searchParam("page", { schema: Schema.NumberFromString }) expect(r.get(page)).toEqual(Option.some(6)) } finally { r.dispose() if (typeof previousWindow === "undefined") { delete (globalThis as any).window } else { Object.defineProperty(globalThis, "window", { value: previousWindow, configurable: true, writable: true }) } } }) it("runtime", async () => { const count = counterRuntime.atom(Counter.use((_) => _.get)).pipe( Atom.withLabel("count") ) const r = AtomRegistry.make() const result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(1) }) it("runtime replacement", async () => { const count = counterRuntime.atom(Counter.use((_) => _.get)) const r = AtomRegistry.make({ initialValues: [Atom.initialValue(counterRuntime.layer, CounterTest)] }) const result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(10) }) it("runtime replacement", async () => { const count = Atom.fnSync((x) => x, { initialValue: 0 }) const r = AtomRegistry.make({ initialValues: [Atom.initialValue(count, 10)] }) const result = r.get(count) expect(result).toEqual(10) r.set(count, 20) const result2 = r.get(count) expect(result2).toEqual(20) }) it("runtime multiple", async () => { const buildCount = buildCounterRuntime.fn()((_) => BuildCounter.use((_) => _.get)) const count = counterRuntime.atom(Counter.use((_) => _.get)) const timesTwo = multiplierRuntime.atom((get) => Effect.gen(function*() { const counter = yield* Counter const multiplier = yield* Multiplier yield* counter.inc expect(yield* get.result(count)).toEqual(2) return yield* multiplier.times(2) }) ) const r = AtomRegistry.make() const cancel = r.mount(buildCount) let result = r.get(timesTwo) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(4) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(2) r.set(buildCount, void 0) assert.deepStrictEqual(r.get(buildCount), AsyncResult.success(1)) await Effect.runPromise(Effect.yieldNow) await Effect.runPromise(Effect.yieldNow) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(1) r.set(buildCount, void 0) assert.deepStrictEqual(r.get(buildCount), AsyncResult.success(2)) cancel() }) it("runtime direct tag", async () => { const counter = counterRuntime.atom(Counter) const r = AtomRegistry.make() const result = r.get(counter) assert(AsyncResult.isSuccess(result)) assert(Effect.isEffect(result.value.get)) }) it("effect initial", async () => { const count = Atom.make( Effect.succeed(1).pipe(Effect.delay(100)), { initialValue: 0 } ).pipe(Atom.keepAlive) const r = AtomRegistry.make() let result = r.get(count) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 0) await vitest.advanceTimersByTimeAsync(100) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(1) }) it("effectFn", async () => { const count = Atom.fn((n: number) => Effect.succeed(n + 1)) const r = AtomRegistry.make() let result = r.get(count) assert(AsyncResult.isInitial(result)) r.set(count, 1) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(2) }) it("effectFn concurrent", async () => { const latches = Arr.empty() let done = 0 const count = Atom.fn((_: number) => { const latch = Latch.makeUnsafe() latches.push(latch) return latch.await.pipe( Effect.tap(() => Effect.sync(() => done++)) ) }, { concurrent: true }) const r = AtomRegistry.make() r.mount(count) let result = r.get(count) assert(AsyncResult.isInitial(result)) r.set(count, 1) result = r.get(count) assert(AsyncResult.isInitial(result) && result.waiting) r.set(count, 1) r.set(count, 1) assert(AsyncResult.isInitial(result) && result.waiting) assert.strictEqual(latches.length, 3) assert.strictEqual(done, 0) latches.forEach((latch) => latch.openUnsafe()) await Effect.runPromise(Effect.yieldNow) assert.strictEqual(done, 3) result = r.get(count) assert(AsyncResult.isSuccess(result)) }) it("effectFn initial", async () => { const count = Atom.fn((n: number) => Effect.succeed(n + 1), { initialValue: 0 }) const r = AtomRegistry.make() let result = r.get(count) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 0) r.set(count, 1) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(2) }) it("effect mapResult", async () => { const count = Atom.fn((n: number) => Effect.succeed(n + 1)).pipe( Atom.mapResult((_) => _ + 1) ) const r = AtomRegistry.make() let result = r.get(count) assert(AsyncResult.isInitial(result)) r.set(count, 1) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(3) }) it("effect double mapResult", async () => { const seed = Atom.make(0) const count = Atom.make((get) => Effect.succeed(get(seed) + 1)).pipe( Atom.mapResult((_) => _ + 10), Atom.mapResult((_) => _ + 100) ) const r = AtomRegistry.make() let result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(111) r.set(seed, 1) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(112) }) it("effect double mapResult refresh", async () => { let rebuilds = 0 const count = Atom.make(() => { rebuilds++ return Effect.succeed(1) }).pipe( Atom.mapResult((_) => _ + 10), Atom.mapResult((_) => _ + 100) ) const r = AtomRegistry.make() let result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(111) expect(rebuilds).toEqual(1) r.refresh(count) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(111) expect(rebuilds).toEqual(2) }) it("keeps parent child links when a parent is read more than once", () => { const flag = Atom.make(true) const base = Atom.make(0) const derived = Atom.make((get) => { const value = get(base) if (get(flag)) { get(base) } return value }) const registry = AtomRegistry.make() const unsubscribe = registry.subscribe(derived, () => { }, { immediate: true }) const nodes = registry.getNodes() const baseNode = nodes.get(base) const derivedNode = nodes.get(derived) assert(baseNode !== undefined) assert(derivedNode !== undefined) assert.strictEqual(baseNode.children.has(derivedNode), true) assert.strictEqual(derivedNode.parents.has(baseNode), true) registry.set(flag, false) assert.strictEqual(baseNode.children.has(derivedNode), true) assert.strictEqual(derivedNode.parents.has(baseNode), true) registry.set(base, 1) assert.strictEqual(registry.get(derived), 1) unsubscribe() }) it("refresh derived before mount resolves base effect", async () => { const baseAtom = Atom.make( Effect.succeed("value").pipe(Effect.delay(100)) ) const derivedAtom = Atom.writable( (get) => get(baseAtom), () => { }, (refresh) => refresh(baseAtom) ) const registry = AtomRegistry.make() registry.refresh(derivedAtom) const unmount = registry.mount(derivedAtom) let result = registry.get(derivedAtom) assert(result.waiting) await vitest.advanceTimersByTimeAsync(100) result = registry.get(derivedAtom) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual("value") unmount() }) it("scopedFn", async () => { let finalized = 0 const count = Atom.fn((n: number) => Effect.succeed(n + 1).pipe( Effect.tap( Effect.addFinalizer(() => Effect.sync(() => { finalized++ }) ) ) ) ).pipe(Atom.keepAlive) const r = AtomRegistry.make() let result = r.get(count) assert(AsyncResult.isInitial(result)) await new Promise((resolve) => resolve(null)) expect(finalized).toEqual(0) r.set(count, 1) result = r.get(count) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(2) r.set(count, 2) await new Promise((resolve) => resolve(null)) expect(finalized).toEqual(1) }) it("disposed lifetime apis are no-ops", () => { let context: Atom.AtomContext | undefined const state = Atom.make(0).pipe(Atom.keepAlive) const option = Atom.make>(Option.some(1)).pipe(Atom.keepAlive) const result = Atom.make>(AsyncResult.success(1)).pipe(Atom.keepAlive) const atom = Atom.make((get) => { context = get return get(state) }).pipe(Atom.keepAlive) const registry = AtomRegistry.make() expect(registry.get(atom)).toEqual(0) registry.refresh(atom) assert(context !== undefined) const disposed = context expect(() => disposed.addFinalizer(() => { }) ).not.toThrow() expect(disposed(state)).toEqual(0) expect(disposed.get(state)).toEqual(0) expect(disposed.once(state)).toEqual(0) expect(disposed.self()).toEqual(Option.none()) expect(() => disposed.result(result)).not.toThrow() expect(() => disposed.resultOnce(result)).not.toThrow() expect(() => disposed.setResult(result, AsyncResult.success(2))).not.toThrow() expect(() => disposed.some(option)).not.toThrow() expect(() => disposed.someOnce(option)).not.toThrow() expect(() => disposed.refresh(state)).not.toThrow() expect(() => disposed.refreshSelf()).not.toThrow() expect(() => disposed.mount(state)).not.toThrow() expect(() => disposed.subscribe(state, () => { }) ).not.toThrow() expect(() => disposed.setSelf(1)).not.toThrow() expect(() => disposed.set(state, 1)).not.toThrow() expect(registry.get(state)).toEqual(0) expect(() => disposed.stream(state)).not.toThrow() expect(() => disposed.streamResult(result)).not.toThrow() }) it("disposed lifetime ignores async updates", async () => { const count = Atom.make( Effect.succeed(1).pipe(Effect.delay(100)), { initialValue: 0, uninterruptible: true } ) const registry = AtomRegistry.make() const unmount = registry.mount(count) const initial = registry.get(count) assert(AsyncResult.isSuccess(initial)) expect(initial.waiting).toEqual(true) expect(initial.value).toEqual(0) unmount() await vitest.advanceTimersByTimeAsync(100) await Effect.runPromise(Effect.yieldNow) const next = registry.get(count) assert(AsyncResult.isSuccess(next)) expect(next.waiting).toEqual(true) }) it.effect("stream", () => Effect.gen(function*() { vitest.useRealTimers() const services = yield* Effect.context() const count = Atom.make( Stream.range(0, 2).pipe( Stream.tap(() => AtomRegistry.AtomRegistry), Stream.tap((_) => Effect.sleep(50)), Stream.provideContext(services) ) ) const r = AtomRegistry.make() const unmount = r.mount(count) let result = r.get(count) assert(result.waiting) assert(AsyncResult.isInitial(result)) yield* TestClock.adjust(50) result = r.get(count) assert(result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 0) yield* TestClock.adjust(50) result = r.get(count) assert(result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 1) yield* TestClock.adjust(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 2) unmount() yield* TestClock.adjust(50) result = r.get(count) assert(result.waiting) assert(AsyncResult.isInitial(result)) })) it("stream initial", async () => { const count = Atom.make( Stream.range(1, 2).pipe( Stream.tap(() => Effect.sleep(50)) ), { initialValue: 0 } ) const r = AtomRegistry.make() const unmount = r.mount(count) let result = r.get(count) assert(result.waiting) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 0) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 1) unmount() await new Promise((resolve) => resolve(null)) result = r.get(count) assert(result.waiting) assert(AsyncResult.isSuccess(result)) }) it("streamFn", async () => { const count = Atom.fn((start: number) => Stream.range(start, start + 1).pipe( Stream.tap(() => Effect.sleep(50)) ) ) const r = AtomRegistry.make() const unmount = r.mount(count) let result = r.get(count) assert.strictEqual(result._tag, "Initial") await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert.strictEqual(result._tag, "Initial") r.set(count, 1) result = r.get(count) assert(result.waiting) assert.strictEqual(result._tag, "Initial") await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 1) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 2) r.set(count, 5) result = r.get(count) assert(result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 2) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 5) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, 6) unmount() await Effect.runPromise(Effect.yieldNow) result = r.get(count) assert(AsyncResult.isInitial(result)) }) it("pull", async () => { const count = Atom.pull( Stream.range(0, 1, 1).pipe( Stream.tap(() => Effect.sleep(50)) ) ) const r = AtomRegistry.make() const unmount = r.mount(count) let result = r.get(count) assert(result.waiting) assert(Option.isNone(AsyncResult.value(result))) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: false, items: [0] }) r.set(count, void 0) result = r.get(count) assert(result.waiting) assert.deepEqual(AsyncResult.value(result), Option.some({ done: false, items: [0] })) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: false, items: [0, 1] }) r.set(count, void 0) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: true, items: [0, 1] }) r.refresh(count) result = r.get(count) assert(result.waiting) assert.deepEqual(AsyncResult.value(result), Option.some({ done: true, items: [0, 1] })) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: false, items: [0] }) unmount() await Effect.runPromise(Effect.yieldNow) result = r.get(count) assert(result.waiting) assert(Option.isNone(AsyncResult.value(result))) }) it("pull runtime", async () => { const count = counterRuntime.pull( Counter.use((_) => _.get).pipe( Effect.map((_) => Stream.range(_, 2, 1)), Stream.unwrap, Stream.tap(() => Effect.sleep(50)) ) ) const r = AtomRegistry.make() const unmount = r.mount(count) let result = r.get(count) assert(result.waiting) assert(Option.isNone(AsyncResult.value(result))) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: false, items: [1] }) r.set(count, void 0) result = r.get(count) assert(result.waiting) assert.deepEqual(AsyncResult.value(result), Option.some({ done: false, items: [1] })) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: false, items: [1, 2] }) r.set(count, void 0) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: true, items: [1, 2] }) r.refresh(count) result = r.get(count) assert(result.waiting) assert.deepEqual(AsyncResult.value(result), Option.some({ done: true, items: [1, 2] })) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: false, items: [1] }) unmount() await Effect.runPromise(Effect.yieldNow) result = r.get(count) assert(result.waiting) assert(Option.isNone(AsyncResult.value(result))) }) it("pull refreshable", async () => { const count = Atom.pull(() => Stream.range(1, 2, 1).pipe( Stream.tap(() => Effect.sleep(50)) ) ) const r = AtomRegistry.make() const unmount = r.mount(count) let result = r.get(count) assert(result.waiting) assert(AsyncResult.isInitial(result)) await vitest.advanceTimersByTimeAsync(50) result = r.get(count) assert(!result.waiting) assert(AsyncResult.isSuccess(result)) assert.deepEqual(result.value, { done: false, items: [1] }) unmount() await Effect.runPromise(Effect.yieldNow) result = r.get(count) assert(result.waiting) }) it("family", async () => { const r = AtomRegistry.make() const count = Atom.family((n: number) => Atom.make(n)) const hash = Hash.hash(count(1)) assert.strictEqual(count(1), count(1)) r.set(count(1), 2) assert.strictEqual(r.get(count(1)), 2) const countKeep = Atom.family((n: number) => Atom.make(n).pipe(Atom.keepAlive)) assert.strictEqual(countKeep(1), countKeep(1)) r.get(countKeep(1)) const hashKeep = Hash.hash(countKeep(1)) if (global.gc) { vitest.useRealTimers() await new Promise((resolve) => setTimeout(resolve, 0)) global.gc() assert.notEqual(hash, Hash.hash(count(1))) assert.strictEqual(hashKeep, Hash.hash(countKeep(1))) } }) it("label", async () => { expect( Atom.make(0).pipe(Atom.withLabel("counter")).label![1] ).toMatch(/Atom.test.ts:\d+:\d+/) }) it("batching", async () => { const r = AtomRegistry.make() const state = Atom.make(1).pipe(Atom.keepAlive) const state2 = Atom.make("a").pipe(Atom.keepAlive) let count = 0 const derived = Atom.readable((get) => { count++ return get(state) + get(state2) }) expect(r.get(derived)).toEqual("1a") expect(count).toEqual(1) Atom.batch(() => { r.set(state, 2) r.set(state2, "b") }) expect(count).toEqual(2) expect(r.get(derived)).toEqual("2b") }) it("nested batch", async () => { const r = AtomRegistry.make() const state = Atom.make(1).pipe(Atom.keepAlive) const state2 = Atom.make("a").pipe(Atom.keepAlive) let count = 0 const derived = Atom.readable((get) => { count++ return get(state) + get(state2) }) expect(r.get(derived)).toEqual("1a") expect(count).toEqual(1) Atom.batch(() => { r.set(state, 2) Atom.batch(() => { r.set(state2, "b") }) }) expect(count).toEqual(2) expect(r.get(derived)).toEqual("2b") }) it("read correct updated state in batch", async () => { const r = AtomRegistry.make() const state = Atom.make(1).pipe(Atom.keepAlive) const state2 = Atom.make("a").pipe(Atom.keepAlive) let count = 0 const derived = Atom.readable((get) => { count++ return get(state) + get(state2) }) expect(r.get(derived)).toEqual("1a") expect(count).toEqual(1) Atom.batch(() => { r.set(state, 2) expect(r.get(derived)).toEqual("2a") r.set(state2, "b") }) expect(count).toEqual(3) expect(r.get(derived)).toEqual("2b") expect(count).toEqual(3) }) it("notifies listeners after batch commit", async () => { const r = AtomRegistry.make() const state = Atom.make(1).pipe(Atom.keepAlive) const state2 = Atom.make("a").pipe(Atom.keepAlive) let count = 0 const derived = Atom.readable((get) => { return get(state) + get(state2) }) r.subscribe(derived, () => { count++ }) Atom.batch(() => { r.get(derived) r.set(state, 2) r.get(derived) r.set(state2, "b") }) expect(count).toEqual(1) expect(r.get(derived)).toEqual("2b") }) it("initialValues", async () => { const state = Atom.make(0) const r = AtomRegistry.make({ initialValues: [ Atom.initialValue(state, 10) ] }) expect(r.get(state)).toEqual(10) await Effect.runPromise(Effect.yieldNow) expect(r.get(state)).toEqual(0) }) it("map with initialValue still rerenders when source changes", () => { const state = Atom.make(0).pipe(Atom.keepAlive) const mapped = state.pipe( Atom.map((n) => n + 1), Atom.keepAlive ) const r = AtomRegistry.make({ initialValues: [ Atom.initialValue(mapped, 10) ] }) r.mount(mapped) assert.strictEqual(r.get(mapped), 10) r.set(state, 1) assert.strictEqual(r.get(mapped), 2) }) it("idleTTL", async () => { const state = Atom.make(0) const state2 = Atom.make(0).pipe( Atom.setIdleTTL(10000) ) const state3 = Atom.make(0).pipe( Atom.setIdleTTL(3000) ) const r = AtomRegistry.make({ defaultIdleTTL: 2000 }) r.set(state, 10) r.set(state2, 10) r.set(state3, 10) expect(r.get(state)).toEqual(10) expect(r.get(state2)).toEqual(10) expect(r.get(state3)).toEqual(10) await new Promise((resolve) => resolve(null)) expect(r.get(state)).toEqual(10) expect(r.get(state2)).toEqual(10) expect(r.get(state3)).toEqual(10) await new Promise((resolve) => resolve(null)) await vitest.advanceTimersByTimeAsync(10000) expect(r.get(state)).toEqual(0) expect(r.get(state2)).toEqual(10) expect(r.get(state3)).toEqual(0) await new Promise((resolve) => resolve(null)) await vitest.advanceTimersByTimeAsync(20000) expect(r.get(state)).toEqual(0) expect(r.get(state2)).toEqual(0) expect(r.get(state3)).toEqual(0) }) it("idleTTL fn", async () => { const fn = Atom.fn((n: number) => Effect.succeed(n + 1)).pipe( Atom.setIdleTTL(0) ) const r = AtomRegistry.make({ defaultIdleTTL: 2000 }) let result = r.get(fn) assert(AsyncResult.isInitial(result)) r.set(fn, 1) result = r.get(fn) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(2) await Effect.runPromise(Effect.yieldNow) result = r.get(fn) assert(AsyncResult.isInitial(result)) }) it("idleTTL fnSync", async () => { const fn = Atom.fnSync((n: number) => n + 1).pipe( Atom.setIdleTTL(0) ) const r = AtomRegistry.make({ defaultIdleTTL: 2000 }) let result = r.get(fn) assert(Option.isNone(result)) r.set(fn, 1) result = r.get(fn) assert(Option.isSome(result)) expect(result.value).toEqual(2) await Effect.runPromise(Effect.yieldNow) result = r.get(fn) assert(Option.isNone(result)) }) it("fn", async () => { const count = Atom.fnSync((n: number) => n).pipe(Atom.keepAlive) const r = AtomRegistry.make() assert.deepEqual(r.get(count), Option.none()) r.set(count, 1) assert.deepEqual(r.get(count), Option.some(1)) }) it("fn initial", async () => { const count = Atom.fnSync((n: number) => n, { initialValue: 0 }) const r = AtomRegistry.make() assert.deepEqual(r.get(count), 0) r.set(count, 1) assert.deepEqual(r.get(count), 1) }) it("withFallback", async () => { const count = Atom.make(() => Effect.succeed(1).pipe( Effect.delay(100) ) ).pipe( Atom.withFallback(Atom.make(() => Effect.succeed(0))), Atom.keepAlive ) const r = AtomRegistry.make() assert.deepEqual(r.get(count), AsyncResult.waiting(AsyncResult.success(0))) await vitest.advanceTimersByTimeAsync(100) assert.deepEqual(r.get(count), AsyncResult.success(1)) }) it("failure with previousSuccess", async () => { const count = Atom.fn((i: number) => i === 1 ? Effect.fail("fail") : Effect.succeed(i)) const r = AtomRegistry.make() let result = r.get(count) assert(AsyncResult.isInitial(result)) r.set(count, 0) result = r.get(count) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 0) r.set(count, 1) result = r.get(count) assert(AsyncResult.isFailure(result)) const error = Cause.findErrorOption(result.cause) assert(Option.isSome(error)) assert.strictEqual(error.value, "fail") const value = AsyncResult.value(result) assert(Option.isSome(value)) assert.strictEqual(value.value, 0) }) it("read non-object", () => { const bool = Atom.make(() => true) const r = AtomRegistry.make() assert.strictEqual(r.get(bool), true) }) it("get.stream", async () => { const count = Atom.make(0) const multiplied = Atom.make((get) => get.stream(count).pipe(Stream.map((_) => _ * 2))) const r = AtomRegistry.make() const cancel = r.mount(multiplied) assert.strictEqual(r.get(count), 0) assert.deepStrictEqual(r.get(multiplied), AsyncResult.success(0, { waiting: true })) r.set(count, 1) await Effect.runPromise(Effect.yieldNow) assert.deepStrictEqual(r.get(multiplied), AsyncResult.success(2, { waiting: true })) cancel() }) it("get.streamResult", async () => { const count = Atom.make(0) const multiplied = Atom.make((get) => get.stream(count).pipe(Stream.map((_) => _ * 2))) const plusOne = Atom.make((get) => get.streamResult(multiplied).pipe(Stream.map((_) => _ + 1))) const r = AtomRegistry.make() const cancel = r.mount(plusOne) assert.strictEqual(r.get(count), 0) assert.deepStrictEqual(r.get(plusOne), AsyncResult.success(1, { waiting: true })) r.set(count, 1) await Effect.runPromise(Effect.yieldNow) await Effect.runPromise(Effect.yieldNow) assert.deepStrictEqual(r.get(plusOne), AsyncResult.success(3, { waiting: true })) cancel() }) it("stream failure keeps previousSuccess", async () => { const atom = Atom.make(() => Stream.succeed(1).pipe(Stream.concat(Stream.fail("boom")))) const r = AtomRegistry.make() const cancel = r.mount(atom) await new Promise((resolve) => resolve(null)) const afterFail = r.get(atom) assert(AsyncResult.isFailure(afterFail)) const prev = AsyncResult.value(afterFail) assert(Option.isSome(prev)) assert.strictEqual(prev.value, 1) cancel() }) it("stream empty produces NoSuchElementError", async () => { const atom = Atom.make(Stream.empty) satisfies Atom.Atom< AsyncResult.AsyncResult > const r = AtomRegistry.make() const cancel = r.mount(atom) await vitest.advanceTimersByTimeAsync(0) const result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.deepStrictEqual( AsyncResult.error(result), Option.some(new Cause.NoSuchElementError()) ) cancel() }) it("Option is not an Effect", async () => { const atom = Atom.make(Option.none()) const r = AtomRegistry.make() assert.deepStrictEqual(r.get(atom), Option.none()) }) it("Either is not an Effect", async () => { const atom = Atom.make(Result.succeed(123)) const r = AtomRegistry.make() assert.deepStrictEqual(r.get(atom), Result.succeed(123)) }) it("SubscriptionRef", async () => { vitest.useRealTimers() const ref = SubscriptionRef.make(123).pipe(Effect.runSync) const atom = Atom.subscriptionRef(ref) const r = AtomRegistry.make() assert.deepStrictEqual(r.get(atom), 123) await Effect.runPromise(SubscriptionRef.update(ref, (a) => a + 1)) assert.deepStrictEqual(r.get(atom), 124) }) it("SubscriptionRef", async () => { vitest.useRealTimers() const ref = SubscriptionRef.make(0).pipe(Effect.runSync) const atom = Atom.subscriptionRef(ref) const r = AtomRegistry.make() const unmount = r.mount(atom) assert.deepStrictEqual(r.get(atom), 0) r.set(atom, 1) await new Promise((resolve) => resolve(null)) assert.deepStrictEqual(r.get(atom), 1) unmount() }) it("SubscriptionRef/effect", async () => { const atom = Atom.subscriptionRef(SubscriptionRef.make(0)) const r = AtomRegistry.make() const unmount = r.mount(atom) assert.deepStrictEqual(r.get(atom), AsyncResult.success(0, { waiting: true })) r.set(atom, 1) await new Promise((resolve) => resolve(null)) assert.deepStrictEqual(r.get(atom), AsyncResult.success(1, { waiting: true })) unmount() }) it("SubscriptionRef/runtime", async () => { const atom = counterRuntime.subscriptionRef(SubscriptionRef.make(0)) const r = AtomRegistry.make() const unmount = r.mount(atom) assert.deepStrictEqual(r.get(atom), AsyncResult.success(0, { waiting: true })) r.set(atom, 1) await new Promise((resolve) => resolve(null)) assert.deepStrictEqual(r.get(atom), AsyncResult.success(1, { waiting: true })) unmount() }) it("SubscriptionRef/runtime/scoped", async () => { let finalized = false const atom = counterRuntime.subscriptionRef( Effect.gen(function*() { yield* Effect.addFinalizer(() => Effect.sync(() => { finalized = true }) ) return yield* SubscriptionRef.make(0) }) ) const r = AtomRegistry.make() const unmount = r.mount(atom) assert.deepStrictEqual(r.get(atom), AsyncResult.success(0, { waiting: true })) r.set(atom, 1) await new Promise((resolve) => resolve(null)) assert.deepStrictEqual(r.get(atom), AsyncResult.success(1, { waiting: true })) assert.strictEqual(finalized, false) unmount() await Effect.runPromise(Effect.yieldNow) assert.strictEqual(finalized, true) }) it("setLazy(true)", async () => { const count = Atom.make(0).pipe(Atom.keepAlive) let rebuilds = 0 const double = Atom.make((get) => { rebuilds++ return get(count) * 2 }).pipe(Atom.keepAlive) const r = AtomRegistry.make() assert.strictEqual(r.get(double), 0) r.set(count, 1) assert.strictEqual(rebuilds, 1) assert.strictEqual(r.get(double), 2) assert.strictEqual(rebuilds, 2) }) it("setLazy(false)", async () => { const count = Atom.make(0).pipe(Atom.keepAlive) let rebuilds = 0 const double = Atom.make((get) => { rebuilds++ return get(count) * 2 }).pipe(Atom.setLazy(false), Atom.keepAlive) const r = AtomRegistry.make() assert.strictEqual(r.get(double), 0) r.set(count, 1) assert.strictEqual(rebuilds, 2) assert.strictEqual(r.get(double), 2) assert.strictEqual(rebuilds, 2) }) it("derived derived with with effect result", async () => { const r = AtomRegistry.make() const state = Atom.fn(Effect.succeed) let count = 0 const derived = Atom.readable((get) => { count++ return get(state).pipe(AsyncResult.getOrElse(() => -1)) % 3 }) let count2 = 0 const derived2 = Atom.readable((get) => { count2++ return get(derived) + 10 }) const cancel = r.mount(derived2) expect(r.get(derived)).toEqual(-1) expect(count).toEqual(1) expect(r.get(derived2)).toEqual(9) expect(count2).toEqual(1) r.set(state, 2) expect(r.get(derived)).toEqual(2) expect(count).toEqual(2) expect(r.get(derived2)).toEqual(12) expect(count2).toEqual(2) r.set(state, 5) expect(r.get(derived)).toEqual(2) expect(count).toEqual(3) expect(r.get(derived2)).toEqual(12) expect(count2).toEqual(2) cancel() }) test(`toStreamResult`, async () => { const r = AtomRegistry.make() const atom = Atom.make(Effect.succeed(1)) const eff = Atom.toStreamResult(atom).pipe( Stream.runHead, Effect.provideService(AtomRegistry.AtomRegistry, r) ) const result = await Effect.runPromise(eff) expect(Option.getOrThrow(result)).toEqual(1) }) test(`refreshOnSignal`, async () => { const r = AtomRegistry.make() let rebuilds = 0 const signal = Atom.make(0) const refreshOnSignal = Atom.makeRefreshOnSignal(signal) const atom = Atom.make(() => { rebuilds++ return 123 }).pipe(refreshOnSignal) r.mount(atom) assert.strictEqual(r.get(atom), 123) assert.strictEqual(rebuilds, 1) r.get(atom) assert.strictEqual(rebuilds, 1) r.set(signal, 1) assert.strictEqual(rebuilds, 2) }) test(`refreshOnSignal uses registry initial values as source state`, () => { let rebuilds = 0 let value = 0 const signal = Atom.make(0) const source = Atom.make(() => { rebuilds++ return value }).pipe(Atom.keepAlive) const atom = source.pipe( Atom.makeRefreshOnSignal(signal), Atom.keepAlive ) const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, 10)] }) r.mount(atom) assert.strictEqual(r.get(atom), 10) assert.strictEqual(r.get(source), 10) assert.strictEqual(rebuilds, 1) value = 11 r.set(signal, 1) assert.strictEqual(r.get(atom), 11) assert.strictEqual(r.get(source), 11) assert.strictEqual(rebuilds, 2) }) test(`debounce uses registry initial values as source state`, async () => { const source = Atom.make(0).pipe(Atom.keepAlive) const atom = source.pipe( Atom.debounce(100), Atom.keepAlive ) const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, 10)] }) r.mount(atom) assert.strictEqual(r.get(atom), 10) assert.strictEqual(r.get(source), 10) r.set(source, 11) assert.strictEqual(r.get(atom), 10) await vitest.advanceTimersByTimeAsync(100) assert.strictEqual(r.get(atom), 11) assert.strictEqual(r.get(source), 11) }) test(`withRefresh uses registry initial values as source state`, async () => { let rebuilds = 0 let value = 0 const source = Atom.make(() => { rebuilds++ return value }).pipe(Atom.keepAlive) const atom = source.pipe( Atom.withRefresh(100), Atom.keepAlive ) const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, 10)] }) r.mount(atom) assert.strictEqual(r.get(atom), 10) assert.strictEqual(r.get(source), 10) assert.strictEqual(rebuilds, 1) value = 11 await vitest.advanceTimersByTimeAsync(100) assert.strictEqual(r.get(atom), 11) assert.strictEqual(r.get(source), 11) assert.strictEqual(rebuilds, 2) }) test(`swr refresh is forceful while fresh`, () => { const r = AtomRegistry.make() let runs = 0 const atom = Atom.make(Effect.sync(() => ++runs)).pipe( Atom.swr({ staleTime: 1_000 }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) r.refresh(atom) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) unmount() }) test(`swr keeps previous value while stale revalidation runs`, async () => { const r = AtomRegistry.make() let runs = 0 const atom = Atom.make(Effect.sync(() => ++runs).pipe(Effect.delay(50))).pipe( Atom.swr({ staleTime: 100 }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(result.waiting) await vitest.advanceTimersByTimeAsync(50) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) await vitest.advanceTimersByTimeAsync(101) r.refresh(atom) result = r.get(atom) assert(result.waiting) assert.deepEqual(AsyncResult.value(result), Option.some(1)) assert.strictEqual(runs, 1) await vitest.advanceTimersByTimeAsync(50) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) unmount() }) test(`swr refresh is forceful after failure with previousSuccess`, () => { const r = AtomRegistry.make() let runs = 0 const atom = Atom.fn((i: number) => { runs++ return i === 1 ? Effect.fail("fail") : Effect.succeed(i) }).pipe( Atom.swr({ staleTime: 1_000 }) ) const unmount = r.mount(atom) r.set(atom, 0) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 0) assert.strictEqual(runs, 1) r.set(atom, 1) result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 2) r.refresh(atom) result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 3) r.refresh(atom) result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 4) unmount() }) test(`swr refreshes failure without previousSuccess`, () => { const r = AtomRegistry.make() let runs = 0 const atom = Atom.make(() => { runs++ return Effect.fail("fail") }).pipe( Atom.swr({ staleTime: 1_000, revalidateOnMount: false }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 1) r.refresh(atom) result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 2) unmount() }) test(`swr revalidateOnMount false skips first read only`, () => { const r = AtomRegistry.make() const focusSignal = Atom.make(0) let focus = 0 const emitFocus = () => r.set(focusSignal, ++focus) let runs = 0 const atom = Atom.make(() => { runs++ return Effect.fail("fail") }).pipe( Atom.swr({ staleTime: 1_000, revalidateOnMount: false, revalidateOnFocus: true, focusSignal }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 1) emitFocus() result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 2) unmount() }) test(`swr auto revalidates failure with previousSuccess only when stale`, async () => { const r = AtomRegistry.make() const focusSignal = Atom.make(0) let focus = 0 const emitFocus = () => r.set(focusSignal, ++focus) let runs = 0 const atom = Atom.fn((i: number) => { runs++ return i === 0 ? Effect.succeed(i) : Effect.fail("fail") }).pipe( Atom.swr({ staleTime: 1_000, revalidateOnMount: false, revalidateOnFocus: true, focusSignal }) ) const unmount = r.mount(atom) r.set(atom, 0) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 0) assert.strictEqual(runs, 1) r.set(atom, 1) result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 2) emitFocus() result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 2) await vitest.advanceTimersByTimeAsync(1_001) emitFocus() result = r.get(atom) assert(AsyncResult.isFailure(result)) assert.strictEqual(runs, 3) unmount() }) test(`swr does not refresh from initial state`, () => { const r = AtomRegistry.make() let runs = 0 const atom = Atom.fn((n: number) => { runs++ return Effect.succeed(n) }).pipe( Atom.swr({ staleTime: 0 }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isInitial(result)) assert.strictEqual(result.waiting, false) assert.strictEqual(runs, 0) r.refresh(atom) result = r.get(atom) assert(AsyncResult.isInitial(result)) assert.strictEqual(result.waiting, false) assert.strictEqual(runs, 0) unmount() }) test(`swr refresh is forceful while waiting`, async () => { const r = AtomRegistry.make() let runs = 0 const atom = Atom.make(Effect.sync(() => ++runs).pipe(Effect.tap(() => Effect.sleep(50)))).pipe( Atom.swr({ staleTime: 0 }) ) const unmount = r.mount(atom) await vitest.advanceTimersByTimeAsync(50) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) r.refresh(atom) result = r.get(atom) assert(result.waiting) assert.strictEqual(runs, 2) r.refresh(atom) result = r.get(atom) assert(result.waiting) assert.strictEqual(runs, 3) await vitest.advanceTimersByTimeAsync(50) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 3) unmount() }) test(`swr delegates refresh to wrapped custom refresh`, () => { const r = AtomRegistry.make() let runs = 0 const source = Atom.make(Effect.sync(() => ++runs)) const proxy = Atom.writable, void>( (get) => get(source), () => { }, (refresh) => refresh(source) ) const atom = proxy.pipe(Atom.swr({ staleTime: 1_000, revalidateOnMount: false })) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) r.refresh(atom) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) unmount() }) test(`swr revalidates on stale remount when enabled`, async () => { const r = AtomRegistry.make() let runs = 0 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive) const atom = base.pipe(Atom.swr({ staleTime: 100, revalidateOnMount: true })) const unmount1 = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) unmount1() await Effect.runPromise(Effect.yieldNow) await vitest.advanceTimersByTimeAsync(101) const unmount2 = r.mount(atom) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) unmount2() }) test(`swr does not revalidate on fresh remount when enabled`, async () => { const r = AtomRegistry.make() let runs = 0 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive) const atom = base.pipe(Atom.swr({ staleTime: 10_000, revalidateOnMount: true })) const unmount1 = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) unmount1() await Effect.runPromise(Effect.yieldNow) const unmount2 = r.mount(atom) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) unmount2() }) test(`swr revalidates on focus signal only when stale`, async () => { const r = AtomRegistry.make() const focusSignal = Atom.make(0) let focus = 0 const emitFocus = () => r.set(focusSignal, ++focus) let runs = 0 const atom = Atom.make(Effect.sync(() => ++runs)).pipe( Atom.swr({ staleTime: 1_000, revalidateOnFocus: true, focusSignal }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) emitFocus() result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) await vitest.advanceTimersByTimeAsync(1_001) emitFocus() result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) unmount() }) test(`swr can force refresh on focus signal`, () => { const r = AtomRegistry.make() const focusSignal = Atom.make(0) let focus = 0 const emitFocus = () => r.set(focusSignal, ++focus) let runs = 0 const atom = Atom.make(Effect.sync(() => ++runs)).pipe( Atom.swr({ staleTime: 1_000, revalidateOnFocus: "always", focusSignal }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) emitFocus() result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) unmount() }) test(`swr treats value as stale at exact staleTime boundary`, async () => { const r = AtomRegistry.make() const focusSignal = Atom.make(0) let focus = 0 const emitFocus = () => r.set(focusSignal, ++focus) let runs = 0 const atom = Atom.make(Effect.sync(() => ++runs)).pipe( Atom.swr({ staleTime: 1_000, revalidateOnFocus: true, focusSignal }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) await vitest.advanceTimersByTimeAsync(1_000) emitFocus() result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) unmount() }) test(`swr does not refresh on focus signal when disabled or omitted`, () => { { const r = AtomRegistry.make() const focusSignal = Atom.make(0) let focus = 0 const emitFocus = () => r.set(focusSignal, ++focus) let runs = 0 const atom = Atom.make(Effect.sync(() => ++runs)).pipe( Atom.swr({ staleTime: 10_000, revalidateOnFocus: false, focusSignal }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) emitFocus() result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) unmount() } { const r = AtomRegistry.make() const focusSignal = Atom.make(0) let focus = 0 const emitFocus = () => r.set(focusSignal, ++focus) let runs = 0 const atom = Atom.make(Effect.sync(() => ++runs)).pipe( Atom.swr({ staleTime: 10_000, focusSignal }) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) emitFocus() result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) unmount() } }) test(`swr does not revalidate on stale remount when disabled`, async () => { const r = AtomRegistry.make() let runs = 0 const base = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive) const atom = base.pipe(Atom.swr({ staleTime: 100, revalidateOnMount: false })) const unmount1 = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) unmount1() await Effect.runPromise(Effect.yieldNow) await vitest.advanceTimersByTimeAsync(101) const unmount2 = r.mount(atom) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) unmount2() }) test(`swr composes with signal driven refresh wrappers`, async () => { const r = AtomRegistry.make() let runs = 0 const signal = Atom.make(0) const atom = Atom.make(Effect.sync(() => ++runs)).pipe( Atom.swr({ staleTime: 1_000 }), Atom.makeRefreshOnSignal(signal) ) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(runs, 1) r.set(signal, 1) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) r.set(signal, 2) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 3) assert.strictEqual(runs, 3) unmount() }) test(`swr preserves writable set semantics`, () => { const r = AtomRegistry.make() let writes = 0 const source = Atom.writable, number>( (get) => Option.getOrElse(get.self>(), () => AsyncResult.success(0)), (ctx, value) => { writes++ ctx.setSelf(AsyncResult.success(value)) } ) const atom = source.pipe(Atom.swr({ staleTime: 1_000 })) const unmount = r.mount(atom) let result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 0) r.set(atom, 1) result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) assert.strictEqual(writes, 1) const sourceResult = r.get(source) assert(AsyncResult.isSuccess(sourceResult)) assert.strictEqual(sourceResult.value, 1) unmount() }) test(`swr uses registry initial values as source state`, () => { let runs = 0 const source = Atom.make(Effect.sync(() => ++runs)).pipe(Atom.keepAlive) const atom = source.pipe( Atom.swr({ staleTime: 1_000, revalidateOnMount: false }), Atom.keepAlive ) const initial = AsyncResult.success(10) const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, initial)] }) r.mount(atom) assert.deepStrictEqual(r.get(atom), initial) assert.deepStrictEqual(r.get(source), initial) assert.strictEqual(runs, 1) r.refresh(atom) const result = r.get(atom) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 2) assert.strictEqual(runs, 2) }) // it("dehydrate", async () => { // const r = AtomRegistry.make() // const notSerializable = Atom.make(0) // r.mount(notSerializable) // // const basicSerializable = Atom.make(0).pipe(Atom.serializable({ // key: "basicSerializable", // schema: Schema.Number // })) // r.mount(basicSerializable) // // const errored = Atom.make(Effect.fail("error")).pipe( // Atom.serializable({ // key: "errored", // schema: AsyncResult.Schema({ // error: Schema.String // }) // }) // ) // r.mount(errored) // // const success = Atom.make(Effect.succeed(123)).pipe(Atom.serializable({ // key: "success", // schema: AsyncResult.Schema({ // success: Schema.Number // }) // })) // r.mount(success) // // const { promise, resolve } = Promise.withResolvers() // // const pending = Atom.make(Effect.promise(() => promise)).pipe(Atom.serializable({ // key: "pending", // schema: AsyncResult.Schema({ // success: Schema.Number // }) // })) // r.mount(pending) // // const state = Hydration.toValues(Hydration.dehydrate(r, { // encodeInitialAs: "promise" // })) // expect(state.map((r) => Struct.omit(r, "dehydratedAt", "resultPromise"))).toMatchInlineSnapshot(` // [ // { // "key": "basicSerializable", // "value": 0, // "~@effect-atom/atom/DehydratedAtom": true, // }, // { // "key": "errored", // "value": { // "_tag": "Failure", // "cause": { // "_tag": "Fail", // "error": "error", // }, // "previousSuccess": { // "_tag": "None", // }, // "waiting": false, // }, // "~@effect-atom/atom/DehydratedAtom": true, // }, // { // "key": "success", // "value": { // "_tag": "Success", // "timestamp": ${Date.now()}, // "value": 123, // "waiting": false, // }, // "~@effect-atom/atom/DehydratedAtom": true, // }, // { // "key": "pending", // "value": { // "_tag": "Initial", // "waiting": true, // }, // "~@effect-atom/atom/DehydratedAtom": true, // }, // ] // `) // // expect(state.find((r) => r.key === "pending")?.resultPromise).instanceOf(Promise) // // const r2 = AtomRegistry.make() // Hydration.hydrate(r2, state) // // expect(r2.get(notSerializable)).toEqual(0) // expect(r2.get(basicSerializable)).toEqual(0) // expect(r2.get(errored)).toEqual(AsyncResult.failure(Cause.fail("error"))) // expect(r2.get(success)).toEqual(AsyncResult.success(123)) // expect(r2.get(pending)).toEqual(AsyncResult.initial(true)) // // resolve(123) // await expect(state.find((r) => r.key === "pending")?.resultPromise).resolves.toEqual({ // "_tag": "Success", // "timestamp": expect.any(Number), // "value": 123, // "waiting": false // }) // }) describe("optimistic", () => { it("non-Result", async () => { const latch = Latch.makeUnsafe() const r = AtomRegistry.make() let i = 0 const atom = Atom.make(() => i) const optimisticAtom = atom.pipe(Atom.optimistic) const fn = optimisticAtom.pipe( Atom.optimisticFn({ reducer: (_current, update: number) => update, fn: Atom.fn(Effect.fnUntraced(function*() { yield* latch.await })) }), Atom.keepAlive ) expect(r.get(atom)).toEqual(0) expect(r.get(optimisticAtom)).toEqual(0) r.set(fn, 1) i = 2 // optimistic phase: the optimistic value is set, but the true value is not expect(r.get(atom)).toEqual(0) expect(r.get(optimisticAtom)).toEqual(1) latch.openUnsafe() await Effect.runPromise(Effect.yieldNow) // commit phase: a refresh is triggered, the authoritative value is used expect(r.get(atom)).toEqual(2) expect(r.get(optimisticAtom)).toEqual(2) }) it("Result", async () => { const runtime = Atom.runtime(Layer.empty) const latch = Latch.makeUnsafe() const r = AtomRegistry.make() let i = 0 const atom = Atom.make(Effect.sync(() => { return i })) const optimisticAtom = atom.pipe( Atom.optimistic ) const fn = optimisticAtom.pipe( Atom.optimisticFn({ reducer: (_current, update: number) => AsyncResult.success(update), fn: runtime.fn(Effect.fnUntraced(function*() { yield* latch.await })) }) ) r.mount(optimisticAtom) r.mount(fn) expect(r.get(atom)).toEqual(AsyncResult.success(0)) expect(r.get(optimisticAtom)).toEqual(AsyncResult.success(0)) r.set(fn, 1) i = 2 // optimistic phase: the optimistic value is set, but the true value is not expect(r.get(atom)).toEqual(AsyncResult.success(0)) expect(r.get(optimisticAtom)).toEqual(AsyncResult.success(1, { waiting: true })) latch.openUnsafe() await Effect.runPromise(Effect.yieldNow) // commit phase: a refresh is triggered, the authoritative value is used expect(r.get(atom)).toEqual(AsyncResult.success(2)) expect(r.get(optimisticAtom)).toEqual(AsyncResult.success(2)) }) it("failures", async () => { const latch = Latch.makeUnsafe() const r = AtomRegistry.make() const i = 0 let rebuilds = 0 const atom = Atom.make(() => { rebuilds++ return i }) const optimisticAtom = atom.pipe( Atom.optimistic ) const fn = optimisticAtom.pipe( Atom.optimisticFn({ reducer: (_, value) => value, fn: Atom.fn()(Effect.fnUntraced(function*() { yield* latch.await return yield* Effect.fail("error") })) }) ) r.mount(fn) r.mount(optimisticAtom) expect(r.get(atom)).toEqual(0) expect(r.get(optimisticAtom)).toEqual(0) r.set(fn, 1) // optimistic phase: the optimistic value is set, but the true value is not expect(r.get(atom)).toEqual(0) expect(r.get(optimisticAtom)).toEqual(1) latch.openUnsafe() await Effect.runPromise(Effect.yieldNow) // commit phase: the optimistic value is reset to the true value expect(r.get(atom)).toEqual(0) expect(r.get(optimisticAtom)).toEqual(0) expect(rebuilds).toEqual(1) }) it("sync fn", async () => { const r = AtomRegistry.make() let i = 0 const atom = Atom.make(() => i) const optimisticAtom = atom.pipe(Atom.optimistic, Atom.keepAlive) const fn = optimisticAtom.pipe( Atom.optimisticFn({ reducer: (_current, update) => update, fn: Atom.fn()(() => { i = 2 return Effect.void }) }) ) expect(r.get(atom)).toEqual(0) expect(r.get(optimisticAtom)).toEqual(0) r.set(fn, 1) expect(r.get(atom)).toEqual(2) expect(r.get(optimisticAtom)).toEqual(2) }) it("intermediate updates", async () => { const latch = Latch.makeUnsafe() const r = AtomRegistry.make() let i = 0 const atom = Atom.make(Effect.sync(() => i)) const optimisticAtom = atom.pipe( Atom.optimistic ) const fn = optimisticAtom.pipe( Atom.optimisticFn({ reducer: (_current, update: number) => AsyncResult.success(update), fn: (set) => Atom.fn(Effect.fnUntraced(function*() { set(AsyncResult.success(123)) yield* latch.await })) }), Atom.keepAlive ) expect(r.get(atom)).toEqual(AsyncResult.success(0)) assert.deepStrictEqual(r.get(optimisticAtom), AsyncResult.success(0)) r.set(fn, 1) i = 2 // optimistic phase: the intermediate value is set, but the true value is // not assert.deepStrictEqual(r.get(atom), AsyncResult.success(0)) assert.deepStrictEqual(r.get(optimisticAtom), AsyncResult.success(123, { waiting: true })) latch.openUnsafe() await Effect.runPromise(Effect.yieldNow) // commit phase: a refresh is triggered, the authoritative value is used assert.deepStrictEqual(r.get(atom), AsyncResult.success(2)) assert.deepStrictEqual(r.get(optimisticAtom), AsyncResult.success(2)) }) }) describe("Reactivity", () => { it("rebuilds on mutation", async () => { const r = AtomRegistry.make() let rebuilds = 0 const atom = Atom.make(() => rebuilds++).pipe( Atom.withReactivity(["counter"]), Atom.keepAlive ) const fn = counterRuntime.fn( Effect.fn(function*() { }), { reactivityKeys: ["counter"] } ) assert.strictEqual(r.get(atom), 0) r.set(fn, void 0) assert.strictEqual(r.get(atom), 1) r.set(fn, void 0) r.set(fn, void 0) assert.strictEqual(r.get(atom), 3) }) it("rebuilds on mutation with a registry initial value", async () => { let rebuilds = 0 let value = 0 const atom = Atom.make(() => { rebuilds++ return value }).pipe( Atom.withReactivity(["counter"]), Atom.keepAlive ) const r = AtomRegistry.make({ initialValues: [Atom.initialValue(atom, 10)] }) const fn = counterRuntime.fn( Effect.fn(function*() { }), { reactivityKeys: ["counter"] } ) r.mount(atom) assert.strictEqual(r.get(atom), 10) assert.strictEqual(rebuilds, 1) value = 11 r.set(fn, void 0) assert.strictEqual(r.get(atom), 11) assert.strictEqual(rebuilds, 2) }) }) it("Atom.Interrupt", async () => { const r = AtomRegistry.make() const atom = Atom.fn(() => Effect.never) r.mount(atom) expect(r.get(atom)).toEqual(AsyncResult.initial()) expect(r.get(atom).waiting).toBeFalsy() r.set(atom, void 0) expect(r.get(atom)).toEqual(AsyncResult.initial(true)) expect(r.get(atom).waiting).toBeTruthy() r.set(atom, Atom.Interrupt) await Effect.runPromise(Effect.yieldNow) const result = r.get(atom) expect(AsyncResult.isInterrupted(result)).toBeTruthy() }) it("writable derived clears waiting after refresh", async () => { let count = 0 const base = Atom.make(Effect.sync(() => ++count).pipe(Effect.delay(100))).pipe( Atom.withLabel("base") ) const derived = Atom.writable( (get) => get(base), () => {}, (refresh) => refresh(base) ).pipe( Atom.withLabel("derived") ) const r = AtomRegistry.make() const unmount1 = r.mount(derived) await vitest.advanceTimersByTimeAsync(100) let result = r.get(derived) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(1) expect(result.waiting).toEqual(false) unmount1() r.refresh(derived) const unmount2 = r.mount(derived) await vitest.advanceTimersByTimeAsync(100) result = r.get(derived) expect(result.waiting).toEqual(false) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(2) unmount2() }) it("get.result suspendOnWaiting", async () => { const r = AtomRegistry.make() const inner = Atom.make(Effect.succeed(1).pipe(Effect.delay(50))) const outer = Atom.make((get) => get.result(inner, { suspendOnWaiting: true })) r.mount(outer) let result = r.get(outer) assert(result.waiting) await vitest.advanceTimersByTimeAsync(50) result = r.get(outer) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value, 1) }) it("fn get.result suspendOnWaiting", async () => { const r = AtomRegistry.make() let runs = 0 const inner = Atom.fn((n: number) => { runs++ return Effect.succeed(n * 2).pipe(Effect.delay(50)) }) const outer = Atom.fn( Effect.fn(function*(_: void, get: Atom.FnContext) { get.set(inner, 1) const a = yield* get.result(inner, { suspendOnWaiting: true }) get.set(inner, 2) const b = yield* get.result(inner, { suspendOnWaiting: true }) return { a, b } }) ) r.mount(outer) r.set(outer, void 0) await vitest.advanceTimersByTimeAsync(100) const result = r.get(outer) assert(AsyncResult.isSuccess(result)) assert.strictEqual(result.value.a, 2) assert.strictEqual(result.value.b, 4) assert.strictEqual(runs, 2) }) describe("kvs", () => { it("memoizes defaultValue while loading empty storage", async () => { let calls = 0 const storage = new Map() const DelayedKVS = Layer.succeed( KeyValueStore.KeyValueStore, KeyValueStore.makeStringOnly({ get: (key) => Effect.gen(function*() { yield* Effect.sleep(20) return storage.get(key) }), set: (key, value) => Effect.sync(() => { storage.set(key, value) }), remove: (key) => Effect.sync(() => { storage.delete(key) }), clear: Effect.sync(() => storage.clear()), size: Effect.sync(() => storage.size) }) ) const kvsRuntime = Atom.runtime(DelayedKVS) const atom = Atom.kvs({ runtime: kvsRuntime, key: "default-value-key", schema: Schema.Number, defaultValue: () => { calls++ return 0 } }) const r = AtomRegistry.make() r.mount(atom) expect(r.get(atom)).toEqual(0) expect(calls).toEqual(1) await vitest.advanceTimersByTimeAsync(50) expect(r.get(atom)).toEqual(0) expect(calls).toEqual(1) }) it("preserves existing value after async load completes", async () => { vitest.useRealTimers() // Create an in-memory store with a pre-existing value const storage = new Map() storage.set("test-key", JSON.stringify(42)) // Create a delayed KeyValueStore to simulate async loading // Use KeyValueStore.make to get proper forSchema support const DelayedKVS = Layer.succeed( KeyValueStore.KeyValueStore, KeyValueStore.makeStringOnly({ get: (key) => Effect.gen(function*() { yield* Effect.sleep(20) // Short delay to create Initial state window return storage.get(key) }), set: (key, value) => Effect.sync(() => { storage.set(key, value) }), remove: (key) => Effect.sync(() => { storage.delete(key) }), clear: Effect.sync(() => storage.clear()), size: Effect.sync(() => storage.size) }) ) const kvsRuntime = Atom.runtime(DelayedKVS) const atom = Atom.kvs({ runtime: kvsRuntime, key: "test-key", schema: Schema.Number, defaultValue: () => 0 }) const r = AtomRegistry.make() r.mount(atom) // First read during Initial state returns default const value = r.get(atom) expect(value).toEqual(0) // Wait for async load AND any set effects to complete await new Promise((resolve) => setTimeout(resolve, 50)) // THE KEY ASSERTION: After load completes, storage should still have original value. // The bug was that the default (0) would be written during Initial state, // corrupting the storage before the async load could read it. expect(storage.get("test-key")).toEqual(JSON.stringify(42)) }) it("async mode", async () => { vitest.useRealTimers() // Create an in-memory store with a pre-existing value const storage = new Map() storage.set("test-key", JSON.stringify(42)) // Create a delayed KeyValueStore to simulate async loading // Use KeyValueStore.make to get proper forSchema support const DelayedKVS = Layer.succeed( KeyValueStore.KeyValueStore, KeyValueStore.makeStringOnly({ get: (key) => Effect.gen(function*() { yield* Effect.sleep(20) // Short delay to create Initial state window return storage.get(key) }), set: (key, value) => Effect.sync(() => { storage.set(key, value) }), remove: (key) => Effect.sync(() => { storage.delete(key) }), clear: Effect.sync(() => storage.clear()), size: Effect.sync(() => storage.size) }) ) const kvsRuntime = Atom.runtime(DelayedKVS) const atom = Atom.kvs({ mode: "async", runtime: kvsRuntime, key: "test-key", schema: Schema.Number, defaultValue: () => 0 }) const r = AtomRegistry.make() r.mount(atom) expect(r.get(atom)).toEqual(AsyncResult.initial(true)) await new Promise((resolve) => setTimeout(resolve, 50)) const result = r.get(atom) assert(AsyncResult.isSuccess(result)) expect(result.value).toEqual(42) expect(result.waiting).toEqual(false) expect(storage.get("test-key")).toEqual(JSON.stringify(42)) r.set(atom, 24) const updated = r.get(atom) assert(AsyncResult.isSuccess(updated)) expect(updated.value).toEqual(24) }) }) }) interface BuildCounter { readonly get: Effect.Effect readonly inc: Effect.Effect } const BuildCounter = Context.Service("BuildCounter") const BuildCounterLive = Layer.sync(BuildCounter, () => { let count = 0 return BuildCounter.of({ get: Effect.sync(() => count), inc: Effect.sync(() => { count++ }) }) }) interface Counter { readonly get: Effect.Effect readonly inc: Effect.Effect } const Counter = Context.Service("Counter") const CounterLive = Layer.effect( Counter, Effect.gen(function*() { const buildCounter = yield* BuildCounter yield* buildCounter.inc let count = 1 return Counter.of({ get: Effect.sync(() => count), inc: Effect.sync(() => { count++ }) }) }) ).pipe( Layer.provide(BuildCounterLive) ) const CounterTest = Layer.effect( Counter, Effect.gen(function*() { const buildCounter = yield* BuildCounter yield* buildCounter.inc let count = 10 return Counter.of({ get: Effect.sync(() => count), inc: Effect.sync(() => { count++ }) }) }) ).pipe( Layer.provide(BuildCounterLive) ) interface Multiplier { readonly times: (n: number) => Effect.Effect } const Multiplier = Context.Service("Multiplier") const MultiplierLive = Layer.effect( Multiplier, Effect.gen(function*() { const counter = yield* Counter yield* AtomRegistry.AtomRegistry // test that we can access the registry return Multiplier.of({ times: (n) => Effect.map(counter.get, (_) => _ * n) }) }) ).pipe( Layer.provideMerge(CounterLive) ) const buildCounterRuntime = Atom.runtime(BuildCounterLive) const counterRuntime = Atom.runtime(CounterLive) const multiplierRuntime = Atom.runtime(MultiplierLive)