import { RegistryContext, useAtom, useAtomInitialValues, useAtomRef, useAtomRefProp, useAtomRefPropValue, useAtomResource, useAtomValue } from "@effect/atom-solid" import { assert, describe, it } from "@effect/vitest" import { AsyncResult, Atom, AtomRef, AtomRegistry } from "effect/unstable/reactivity" import { type Accessor, createComponent, createEffect, createRoot, type Resource } from "solid-js" describe("atom-solid", () => { describe("useAtomValue", () => { it("reads value from simple Atom", () => { const atom = Atom.make(42) let observed: number | undefined const dispose = renderAtomValue(atom, (value) => { observed = value }) assert.strictEqual(observed, 42) dispose() }) it("reads value with transform function", () => { const atom = Atom.make(42) let observed: number | undefined const dispose = renderAtomValue(atom, (value) => { observed = value }, { map: (value) => value * 2 }) assert.strictEqual(observed, 84) dispose() }) it("updates when Atom value changes", () => { const registry = AtomRegistry.make() const atom = Atom.make("initial") let observed: string | undefined const dispose = renderAtomValue(atom, (value) => { observed = value }, { registry }) assert.strictEqual(observed, "initial") registry.set(atom, "updated") assert.strictEqual(observed, "updated") dispose() }) it("works with computed Atom", () => { const baseAtom = Atom.make(10) const computedAtom = Atom.make((get) => get(baseAtom) * 2) let observed: number | undefined const dispose = renderAtomValue(computedAtom, (value) => { observed = value }) assert.strictEqual(observed, 20) dispose() }) }) describe("useAtom", () => { it("updates value with setter", () => { const atom = Atom.make(0) let observed: number | undefined const dispose = createRoot((dispose) => { const [value, setValue] = useAtom(() => atom) createEffect(() => { observed = value() }) createEffect(() => { if (value() !== 0) { return } setValue(1) setValue((current) => current + 1) }) return dispose }) assert.strictEqual(observed, 2) dispose() }) }) describe("useAtomInitialValues", () => { it("applies initial values once per registry", () => { const registry = AtomRegistry.make() const atom = Atom.make(0) createRoot((dispose) => { createComponent(RegistryContext.Provider, { value: registry, get children() { useAtomInitialValues([[atom, 1]]) useAtomInitialValues([[atom, 2]]) assert.strictEqual(registry.get(atom), 1) return null } }) return dispose }) }) }) describe("AtomRef", () => { it("updates when AtomRef changes", () => { const ref = AtomRef.make(0) let observed: number | undefined const dispose = renderAtomRef(ref, (value) => { observed = value }) assert.strictEqual(observed, 0) ref.set(1) assert.strictEqual(observed, 1) dispose() }) it("updates when AtomRef prop changes", () => { const ref = AtomRef.make({ count: 0, label: "a" }) const propRef = useAtomRefProp(() => ref, "count") let observed: number | undefined const dispose = renderAtomRef(propRef(), (value) => { observed = value }) assert.strictEqual(observed, 0) ref.set({ count: 1, label: "a" }) assert.strictEqual(observed, 1) dispose() }) it("updates when AtomRef prop value changes", () => { const ref = AtomRef.make({ count: 0, label: "a" }) let observed: number | undefined const dispose = renderAccessor(() => useAtomRefPropValue(() => ref, "count"), (value) => { observed = value }) assert.strictEqual(observed, 0) ref.set({ count: 2, label: "a" }) assert.strictEqual(observed, 2) dispose() }) }) describe("useAtomResource", () => { it("suspends on Initial result", () => { const atom = Atom.make(AsyncResult.initial()) const { resource, dispose } = renderAtomResource(() => atom) assert.strictEqual(resource.loading, true) assert.strictEqual(resource(), undefined) dispose() }) }) // // it("suspends on waiting when suspendOnWaiting is true", () => { // const atom = Atom.make(AsyncResult.success(1, { waiting: true })) // const { resource, dispose } = renderAtomResource(atom, { suspendOnWaiting: true }) // assert.strictEqual(resource.loading, true) // assert.strictEqual(resource(), undefined) // dispose() // }) // // it("returns success value by default", async () => { // const atom = Atom.make(AsyncResult.success(5, { waiting: true })) // const { resource, dispose } = renderAtomResource(atom) // await Promise.resolve() // await Promise.resolve() // assert.strictEqual(resource.loading, false) // assert.strictEqual(resource(), 5) // dispose() // }) // // it("surfaces failure via Cause.squash", async () => { // const error = new Error("boom") // const atom = Atom.make(AsyncResult.fail(error)) // let resource: ReturnType[0] | undefined // let caught: unknown // const dispose = createRoot((dispose) => { // catchError(() => { // ;[resource] = useAtomResource(atom) // createEffect(() => { // resource?.() // }) // }, (err) => { // caught = err // }) // return dispose // }) // await Promise.resolve() // await Promise.resolve() // assert.ok(caught instanceof Error) // assert.strictEqual(caught.message, "boom") // assert.strictEqual(resource?.error, caught) // assert.strictEqual(resource?.loading, false) // dispose() // }) // // it("preserves success result when preserveResult is true", async () => { // const atom = Atom.make(Effect.succeed(7)) // const { resource, dispose } = renderAtomResource(atom, { preserveResult: true }) // await Promise.resolve() // await Promise.resolve() // const result = resource()! // assert.strictEqual(AsyncResult.isSuccess(result), true) // if (AsyncResult.isSuccess(result)) { // assert.strictEqual(result.value, 7) // } // assert.strictEqual(resource.error, undefined) // dispose() // }) // // it("preserves failure result when preserveResult is true", async () => { // const error = new Error("failure") // const atom = Atom.make(Effect.fail(error)) // const { resource, dispose } = renderAtomResource(atom, { preserveResult: true }) // await Promise.resolve() // await Promise.resolve() // const result = resource()! // assert.strictEqual(AsyncResult.isFailure(result), true) // if (AsyncResult.isFailure(result)) { // const squashed = Cause.squash(result.cause) // assert.ok(squashed instanceof Error) // assert.strictEqual(squashed.message, "failure") // } // assert.strictEqual(resource.error, undefined) // dispose() // }) // }) }) const renderAtomRef = function(ref: AtomRef.ReadonlyRef, onValue: (_: A) => void) { return createRoot((dispose) => { const accessor = useAtomRef(() => ref) createEffect(() => { onValue(accessor()) }) return dispose }) } const renderAccessor = function(makeAccessor: () => Accessor, onValue: (_: A) => void) { return createRoot((dispose) => { const accessor = makeAccessor() createEffect(() => { onValue(accessor()) }) return dispose }) } const renderAtomValue = function( atom: Atom.Atom, onValue: (_: B) => void, options?: { readonly registry?: AtomRegistry.AtomRegistry; readonly map?: (_: A) => B } ) { return createRoot((dispose) => { const run = () => { const accessor = options?.map ? useAtomValue(() => atom, options.map) : useAtomValue(() => atom) createEffect(() => { onValue(accessor() as B) }) return null } if (options?.registry) { createComponent(RegistryContext.Provider, { value: options.registry, get children() { return run() } }) } else { run() } return dispose }) } const renderAtomResource = function( atom: () => Atom.Atom>, options?: { readonly suspendOnWaiting?: boolean | undefined } ) { let resource: | Resource | undefined const dispose = createRoot((dispose) => { ;[resource] = useAtomResource(atom, options) createEffect(() => { resource?.() }) return dispose }) return { resource: resource!, dispose } }