Files

2591 lines
73 KiB
TypeScript

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<number, number>((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<void>()((_) => 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<Latch.Latch>()
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.Option<number>>(Option.some(1)).pipe(Atom.keepAlive)
const result = Atom.make<AsyncResult.AsyncResult<number, never>>(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<number>()).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<never>()
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<never, Cause.NoSuchElementError>
>
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<string>())
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<number>)
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<AsyncResult.AsyncResult<number, never>, 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<AsyncResult.AsyncResult<number, never>, number>(
(get) => Option.getOrElse(get.self<AsyncResult.AsyncResult<number, never>>(), () => 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<number>()
//
// 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<number>()(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<number>()(() => {
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<string, string>()
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<string, string>()
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<string, string>()
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<number>
readonly inc: Effect.Effect<void>
}
const BuildCounter = Context.Service<BuildCounter>("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<number>
readonly inc: Effect.Effect<void>
}
const Counter = Context.Service<Counter>("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<number>
}
const Multiplier = Context.Service<Multiplier>("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)