1import {
2 RegistryContext,
3 useAtom,
4 useAtomInitialValues,
5 useAtomRef,
6 useAtomRefProp,
7 useAtomRefPropValue,
8 useAtomResource,
9 useAtomValue
10} from "@effect/atom-solid"
11import { assert, describe, it } from "@effect/vitest"
12import { AsyncResult, Atom, AtomRef, AtomRegistry } from "effect/reactivity"
13import { type Accessor, createComponent, createEffect, createRoot, type Resource } from "solid-js"
14
15describe("atom-solid", () => {
16 describe("useAtomValue", () => {
17 it("reads value from simple Atom", () => {
18 const atom = Atom.make(42)
19 let observed: number | undefined
20 const dispose = renderAtomValue(atom, (value) => {
21 observed = value
22 })
23 assert.strictEqual(observed, 42)
24 dispose()
25 })
26
27 it("reads value with transform function", () => {
28 const atom = Atom.make(42)
29 let observed: number | undefined
30 const dispose = renderAtomValue(atom, (value) => {
31 observed = value
32 }, { map: (value) => value * 2 })
33 assert.strictEqual(observed, 84)
34 dispose()
35 })
36
37 it("updates when Atom value changes", () => {
38 const registry = AtomRegistry.make()
39 const atom = Atom.make("initial")
40 let observed: string | undefined
41 const dispose = renderAtomValue(atom, (value) => {
42 observed = value
43 }, { registry })
44 assert.strictEqual(observed, "initial")
45 registry.set(atom, "updated")
46 assert.strictEqual(observed, "updated")
47 dispose()
48 })
49
50 it("works with computed Atom", () => {
51 const baseAtom = Atom.make(10)
52 const computedAtom = Atom.make((get) => get(baseAtom) * 2)
53 let observed: number | undefined
54 const dispose = renderAtomValue(computedAtom, (value) => {
55 observed = value
56 })
57 assert.strictEqual(observed, 20)
58 dispose()
59 })
60 })
61
62 describe("useAtom", () => {
63 it("updates value with setter", () => {
64 const atom = Atom.make(0)
65 let observed: number | undefined
66 const dispose = createRoot((dispose) => {
67 const [value, setValue] = useAtom(() => atom)
68 createEffect(() => {
69 observed = value()
70 })
71 createEffect(() => {
72 if (value() !== 0) {
73 return
74 }
75 setValue(1)
76 setValue((current) => current + 1)
77 })
78 return dispose
79 })
80 assert.strictEqual(observed, 2)
81 dispose()
82 })
83 })
84
85 describe("useAtomInitialValues", () => {
86 it("applies initial values once per registry", () => {
87 const registry = AtomRegistry.make()
88 const atom = Atom.make(0)
89 createRoot((dispose) => {
90 createComponent(RegistryContext.Provider, {
91 value: registry,
92 get children() {
93 useAtomInitialValues([[atom, 1]])
94 useAtomInitialValues([[atom, 2]])
95 assert.strictEqual(registry.get(atom), 1)
96 return null
97 }
98 })
99 return dispose
100 })
101 })
102 })
103
104 describe("AtomRef", () => {
105 it("updates when AtomRef changes", () => {
106 const ref = AtomRef.make(0)
107 let observed: number | undefined
108 const dispose = renderAtomRef(ref, (value) => {
109 observed = value
110 })
111 assert.strictEqual(observed, 0)
112 ref.set(1)
113 assert.strictEqual(observed, 1)
114 dispose()
115 })
116
117 it("updates when AtomRef prop changes", () => {
118 const ref = AtomRef.make({ count: 0, label: "a" })
119 const propRef = useAtomRefProp(() => ref, "count")
120 let observed: number | undefined
121 const dispose = renderAtomRef(propRef(), (value) => {
122 observed = value
123 })
124 assert.strictEqual(observed, 0)
125 ref.set({ count: 1, label: "a" })
126 assert.strictEqual(observed, 1)
127 dispose()
128 })
129
130 it("updates when AtomRef prop value changes", () => {
131 const ref = AtomRef.make({ count: 0, label: "a" })
132 let observed: number | undefined
133 const dispose = renderAccessor(() => useAtomRefPropValue(() => ref, "count"), (value) => {
134 observed = value
135 })
136 assert.strictEqual(observed, 0)
137 ref.set({ count: 2, label: "a" })
138 assert.strictEqual(observed, 2)
139 dispose()
140 })
141 })
142
143 describe("useAtomResource", () => {
144 it("suspends on Initial result", () => {
145 const atom = Atom.make(AsyncResult.initial<number, Error>())
146 const { resource, dispose } = renderAtomResource(() => atom)
147 assert.strictEqual(resource.loading, true)
148 assert.strictEqual(resource(), undefined)
149 dispose()
150 })
151 })
152
153 //
154 // it("suspends on waiting when suspendOnWaiting is true", () => {
155 // const atom = Atom.make(AsyncResult.success(1, { waiting: true }))
156 // const { resource, dispose } = renderAtomResource(atom, { suspendOnWaiting: true })
157 // assert.strictEqual(resource.loading, true)
158 // assert.strictEqual(resource(), undefined)
159 // dispose()
160 // })
161 //
162 // it("returns success value by default", async () => {
163 // const atom = Atom.make(AsyncResult.success(5, { waiting: true }))
164 // const { resource, dispose } = renderAtomResource(atom)
165 // await Promise.resolve()
166 // await Promise.resolve()
167 // assert.strictEqual(resource.loading, false)
168 // assert.strictEqual(resource(), 5)
169 // dispose()
170 // })
171 //
172 // it("surfaces failure via Cause.squash", async () => {
173 // const error = new Error("boom")
174 // const atom = Atom.make(AsyncResult.fail(error))
175 // let resource: ReturnType<typeof useAtomResource>[0] | undefined
176 // let caught: unknown
177 // const dispose = createRoot((dispose) => {
178 // catchError(() => {
179 // ;[resource] = useAtomResource(atom)
180 // createEffect(() => {
181 // resource?.()
182 // })
183 // }, (err) => {
184 // caught = err
185 // })
186 // return dispose
187 // })
188 // await Promise.resolve()
189 // await Promise.resolve()
190 // assert.ok(caught instanceof Error)
191 // assert.strictEqual(caught.message, "boom")
192 // assert.strictEqual(resource?.error, caught)
193 // assert.strictEqual(resource?.loading, false)
194 // dispose()
195 // })
196 //
197 // it("preserves success result when preserveResult is true", async () => {
198 // const atom = Atom.make(Effect.succeed(7))
199 // const { resource, dispose } = renderAtomResource(atom, { preserveResult: true })
200 // await Promise.resolve()
201 // await Promise.resolve()
202 // const result = resource()!
203 // assert.strictEqual(AsyncResult.isSuccess(result), true)
204 // if (AsyncResult.isSuccess(result)) {
205 // assert.strictEqual(result.value, 7)
206 // }
207 // assert.strictEqual(resource.error, undefined)
208 // dispose()
209 // })
210 //
211 // it("preserves failure result when preserveResult is true", async () => {
212 // const error = new Error("failure")
213 // const atom = Atom.make(Effect.fail(error))
214 // const { resource, dispose } = renderAtomResource(atom, { preserveResult: true })
215 // await Promise.resolve()
216 // await Promise.resolve()
217 // const result = resource()!
218 // assert.strictEqual(AsyncResult.isFailure(result), true)
219 // if (AsyncResult.isFailure(result)) {
220 // const squashed = Cause.squash(result.cause)
221 // assert.ok(squashed instanceof Error)
222 // assert.strictEqual(squashed.message, "failure")
223 // }
224 // assert.strictEqual(resource.error, undefined)
225 // dispose()
226 // })
227 // })
228})
229
230const renderAtomRef = function<A>(ref: AtomRef.ReadonlyRef<A>, onValue: (_: A) => void) {
231 return createRoot((dispose) => {
232 const accessor = useAtomRef(() => ref)
233 createEffect(() => {
234 onValue(accessor())
235 })
236 return dispose
237 })
238}
239
240const renderAccessor = function<A>(makeAccessor: () => Accessor<A>, onValue: (_: A) => void) {
241 return createRoot((dispose) => {
242 const accessor = makeAccessor()
243 createEffect(() => {
244 onValue(accessor())
245 })
246 return dispose
247 })
248}
249
250const renderAtomValue = function<A, B = A>(
251 atom: Atom.Atom<A>,
252 onValue: (_: B) => void,
253 options?: { readonly registry?: AtomRegistry.AtomRegistry; readonly map?: (_: A) => B }
254) {
255 return createRoot((dispose) => {
256 const run = () => {
257 const accessor = options?.map ? useAtomValue(() => atom, options.map) : useAtomValue(() => atom)
258 createEffect(() => {
259 onValue(accessor() as B)
260 })
261 return null
262 }
263
264 if (options?.registry) {
265 createComponent(RegistryContext.Provider, {
266 value: options.registry,
267 get children() {
268 return run()
269 }
270 })
271 } else {
272 run()
273 }
274
275 return dispose
276 })
277}
278
279const renderAtomResource = function<A, E>(
280 atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>,
281 options?: {
282 readonly suspendOnWaiting?: boolean | undefined
283 }
284) {
285 let resource:
286 | Resource<A>
287 | undefined
288 const dispose = createRoot((dispose) => {
289 ;[resource] = useAtomResource(atom, options)
290 createEffect(() => {
291 resource?.()
292 })
293 return dispose
294 })
295 return { resource: resource!, dispose }
296}