1/**
2 * Solid hooks for using Effect Atoms from components and computations. The
3 * hooks read and write atoms through the current `RegistryContext`, mount atoms
4 * for cleanup, subscribe callbacks, seed initial values, expose `AsyncResult`
5 * atoms as Solid resources, and read values from `AtomRef` references.
6 *
7 * @since 4.0.0
8 */
9import * as Cause from "effect/Cause"
10import * as Effect from "effect/Effect"
11import * as Exit from "effect/Exit"
12import * as AsyncResult from "effect/reactivity/AsyncResult"
13import * as Atom from "effect/reactivity/Atom"
14import type * as AtomRef from "effect/reactivity/AtomRef"
15import * as AtomRegistry from "effect/reactivity/AtomRegistry"
16import type { Accessor, ResourceOptions, ResourceReturn } from "solid-js"
17import { createComputed, createEffect, createMemo, createResource, createSignal, onCleanup, useContext } from "solid-js"
18import { RegistryContext } from "./RegistryContext.ts"
19
20const initialValuesSet = new WeakMap<AtomRegistry.AtomRegistry, WeakSet<Atom.Atom<any>>>()
21
22/**
23 * Seeds initial atom values in the current Solid atom registry.
24 *
25 * **When to use**
26 *
27 * Use to seed atom values from a Solid component after the current registry
28 * already exists.
29 *
30 * **Details**
31 *
32 * For each atom in the current registry, this hook applies the first value
33 * supplied through the hook. Later calls for the same atom in that registry are
34 * ignored.
35 *
36 * @category hooks
37 * @since 4.0.0
38 */
39export const useAtomInitialValues = (initialValues: Iterable<readonly [Atom.Atom<any>, any]>): void => {
40 const registry = useContext(RegistryContext)
41 let set = initialValuesSet.get(registry)
42 if (set === undefined) {
43 set = new WeakSet()
44 initialValuesSet.set(registry, set)
45 }
46 for (const [atom, value] of initialValues) {
47 if (!set.has(atom)) {
48 set.add(atom)
49 ;(registry as any).ensureNode(atom).setValue(value)
50 }
51 }
52}
53
54/**
55 * Subscribes to an atom in the current Solid registry and returns its value as
56 * a Solid accessor.
57 *
58 * @category hooks
59 * @since 4.0.0
60 */
61export const useAtomValue: {
62 <A>(atom: () => Atom.Atom<A>): Accessor<A>
63 <A, B>(atom: () => Atom.Atom<A>, f: (_: A) => B): Accessor<B>
64} = <A>(atom: () => Atom.Atom<A>, f?: (_: A) => A): Accessor<A> => {
65 const registry = useContext(RegistryContext)
66 return createAtomAccessor(registry, f ? () => Atom.map(atom(), f) : atom)
67}
68
69function createAtomAccessor<A>(registry: AtomRegistry.AtomRegistry, atom: () => Atom.Atom<A>): Accessor<A> {
70 const [value, setValue] = createSignal<A>(null as any)
71 createComputed(() => {
72 onCleanup(registry.subscribe(atom(), setValue as any, constImmediate))
73 })
74 return value
75}
76
77const constImmediate = { immediate: true }
78
79function mountAtom<A>(registry: AtomRegistry.AtomRegistry, atom: () => Atom.Atom<A>): void {
80 createComputed(() => {
81 onCleanup(registry.mount(atom()))
82 })
83}
84
85function setAtom<R, W, Mode extends "value" | "promise" | "promiseExit" = never>(
86 registry: AtomRegistry.AtomRegistry,
87 atom: () => Atom.Writable<R, W>,
88 options?: {
89 readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
90 }
91): "promise" extends Mode ? (
92 (value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
93 ) :
94 "promiseExit" extends Mode ? (
95 (value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
96 ) :
97 ((value: W | ((value: R) => W)) => void)
98{
99 const memo = createMemo(atom)
100 if (options?.mode === "promise" || options?.mode === "promiseExit") {
101 return ((value: W) => {
102 registry.set(memo(), value)
103 const promise = Effect.runPromiseExit(
104 AtomRegistry.getResult(registry, memo() as Atom.Atom<AsyncResult.AsyncResult<any, any>>, {
105 suspendOnWaiting: true
106 })
107 )
108 return options!.mode === "promise" ? promise.then(flattenExit) : promise
109 }) as any
110 }
111 return ((value: W | ((value: R) => W)) => {
112 registry.set(memo(), typeof value === "function" ? (value as any)(registry.get(memo())) : value)
113 }) as any
114}
115
116const flattenExit = <A, E>(exit: Exit.Exit<A, E>): A => {
117 if (Exit.isSuccess(exit)) return exit.value
118 throw Cause.squash(exit.cause)
119}
120
121/**
122 * Mounts an atom in the current Solid registry for the lifetime of the current
123 * Solid computation.
124 *
125 * **When to use**
126 *
127 * Use to keep an atom mounted from a Solid owner without reading, writing, or
128 * refreshing it.
129 *
130 * **Details**
131 *
132 * The hook uses the current `RegistryContext`, mounts inside a Solid
133 * computation, and releases the mount through Solid cleanup when the
134 * computation changes or the owner is disposed.
135 *
136 * @see {@link useAtomSet} for mounting a writable atom while returning a setter
137 * @see {@link useAtomRefresh} for mounting an atom while returning a refresh callback
138 *
139 * @category hooks
140 * @since 4.0.0
141 */
142export const useAtomMount = <A>(atom: () => Atom.Atom<A>): void => {
143 const registry = useContext(RegistryContext)
144 mountAtom(registry, atom)
145}
146
147/**
148 * Returns a setter for a writable atom without subscribing to its value.
149 *
150 * @category hooks
151 * @since 4.0.0
152 */
153export const useAtomSet = <
154 R,
155 W,
156 Mode extends "value" | "promise" | "promiseExit" = never
157>(
158 atom: () => Atom.Writable<R, W>,
159 options?: {
160 readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
161 }
162): "promise" extends Mode ? (
163 (value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
164 ) :
165 "promiseExit" extends Mode ? (
166 (value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
167 ) :
168 ((value: W | ((value: R) => W)) => void) =>
169{
170 const registry = useContext(RegistryContext)
171 mountAtom(registry, atom)
172 return setAtom(registry, atom, options)
173}
174
175/**
176 * Mounts an atom and returns a callback that refreshes the current atom.
177 *
178 * @category hooks
179 * @since 4.0.0
180 */
181export const useAtomRefresh = <A>(atom: () => Atom.Atom<A>): () => void => {
182 const registry = useContext(RegistryContext)
183 mountAtom(registry, atom)
184 const memo = createMemo(atom)
185 return () => registry.refresh(memo())
186}
187
188/**
189 * Returns a Solid accessor for a writable atom together with a setter for
190 * updating it.
191 *
192 * **When to use**
193 *
194 * Use when a Solid component or computation needs both a reactive accessor for
195 * a writable atom and a write function for that same atom.
196 *
197 * **Details**
198 *
199 * The setter accepts either a write value or an updater function. For
200 * `AsyncResult` atoms, `promise` and `promiseExit` modes return promises for the
201 * success value or full `Exit`.
202 *
203 * @see {@link useAtomValue} for subscribing to an atom without a setter
204 * @see {@link useAtomSet} for updating a writable atom without subscribing to its value
205 *
206 * @category hooks
207 * @since 4.0.0
208 */
209export const useAtom = <R, W, const Mode extends "value" | "promise" | "promiseExit" = never>(
210 atom: () => Atom.Writable<R, W>,
211 options?: {
212 readonly mode?: ([R] extends [AsyncResult.AsyncResult<any, any>] ? Mode : "value") | undefined
213 }
214): readonly [
215 value: Accessor<R>,
216 write: "promise" extends Mode ? (
217 (value: W) => Promise<AsyncResult.AsyncResult.Success<R>>
218 ) :
219 "promiseExit" extends Mode ? (
220 (value: W) => Promise<Exit.Exit<AsyncResult.AsyncResult.Success<R>, AsyncResult.AsyncResult.Failure<R>>>
221 ) :
222 ((value: W | ((value: R) => W)) => void)
223] => {
224 const registry = useContext(RegistryContext)
225 return [
226 createAtomAccessor(registry, atom),
227 setAtom(registry, atom, options)
228 ] as const
229}
230
231/**
232 * Subscribes a callback to an atom in the current Solid registry.
233 *
234 * @category hooks
235 * @since 4.0.0
236 */
237export const useAtomSubscribe = <A>(
238 atom: () => Atom.Atom<A>,
239 f: (_: A) => void,
240 options?: { readonly immediate?: boolean }
241): void => {
242 const registry = useContext(RegistryContext)
243 createEffect(() => {
244 onCleanup(registry.subscribe(atom(), f, options))
245 })
246}
247
248/**
249 * Converts an `AsyncResult` atom into a Solid resource.
250 *
251 * @category hooks
252 * @since 4.0.0
253 */
254export const useAtomResource = <A, E>(
255 atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>,
256 options?: ResourceOptions<A> & {
257 readonly suspendOnWaiting?: boolean | undefined
258 }
259): ResourceReturn<A, void> => {
260 const result = useAtomValue(atom)
261 return createResource(result, (result) => {
262 if (AsyncResult.isInitial(result) || (options?.suspendOnWaiting && result.waiting)) {
263 return constUnresolvedPromise
264 } else if (AsyncResult.isSuccess(result)) {
265 return Promise.resolve(result.value)
266 }
267 return Promise.reject(Cause.squash(result.cause))
268 })
269}
270
271const constUnresolvedPromise = new Promise<never>(() => {})
272
273/**
274 * Subscribes to an atom ref and returns its value as a Solid accessor.
275 *
276 * **When to use**
277 *
278 * Use when a Solid component or computation should render from an
279 * `AtomRef.ReadonlyRef` directly instead of reading an atom through the current
280 * registry.
281 *
282 * **Details**
283 *
284 * The hook accepts a thunk for the ref, reads `ref().value`, subscribes with
285 * `ref.subscribe`, and releases the subscription through Solid cleanup when
286 * the selected ref changes or the owner is disposed.
287 *
288 * @see {@link useAtomValue} for reading an `Atom` from the current registry
289 * @see {@link useAtomRefPropValue} for reading a property ref value
290 *
291 * @category hooks
292 * @since 4.0.0
293 */
294export const useAtomRef = <A>(ref: () => AtomRef.ReadonlyRef<A>): Accessor<A> => {
295 const [value, setValue] = createSignal(null as A)
296 createComputed(() => {
297 const r = ref()
298 setValue(r.value as any)
299 onCleanup(r.subscribe(setValue))
300 })
301 return value
302}
303
304/**
305 * Returns a Solid accessor for a property ref derived from an atom ref.
306 *
307 * **When to use**
308 *
309 * Use to derive an `AtomRef` for one property of an object-shaped atom ref in a
310 * Solid computation.
311 *
312 * **Details**
313 *
314 * The returned accessor memoizes `ref().prop(prop)`, updating when the source
315 * ref thunk produces a different ref.
316 *
317 * **Gotchas**
318 *
319 * The `prop` argument is captured as a plain value. Recreate the hook call when
320 * the property key should change.
321 *
322 * @see {@link useAtomRef} for subscribing to an atom ref value
323 * @see {@link useAtomRefPropValue} for subscribing directly to a property value
324 *
325 * @category hooks
326 * @since 4.0.0
327 */
328export const useAtomRefProp = <A, K extends keyof A>(
329 ref: () => AtomRef.AtomRef<A>,
330 prop: K
331): Accessor<AtomRef.AtomRef<A[K]>> => createMemo(() => ref().prop(prop))
332
333/**
334 * Returns a Solid accessor for the value of a property ref derived from an atom
335 * ref.
336 *
337 * **When to use**
338 *
339 * Use when a Solid component or computation needs the value of one property
340 * from an object-shaped `AtomRef` without keeping the intermediate property ref.
341 *
342 * **Details**
343 *
344 * The hook composes `useAtomRefProp(ref, prop)` with `useAtomRef`, returning a
345 * Solid accessor for the selected property value.
346 *
347 * **Gotchas**
348 *
349 * The `prop` argument is captured as a plain value. Recreate the hook call when
350 * the property key should change.
351 *
352 * @see {@link useAtomRef} for subscribing to a whole atom ref value
353 * @see {@link useAtomRefProp} for returning the property ref directly
354 *
355 * @category hooks
356 * @since 4.0.0
357 */
358export const useAtomRefPropValue = <A, K extends keyof A>(ref: () => AtomRef.AtomRef<A>, prop: K): Accessor<A[K]> =>
359 useAtomRef(useAtomRefProp(ref, prop))