Skip to content

Commit 57d3c70

Browse files
committed
feat: add SolidJS integration package (@effect-atom/atom-solid)
SolidJS adapter for @effect-atom/atom with hooks: - useAtomValue, useAtom, useAtomSet, useAtomMount, useAtomRefresh - useAtomSubscribe, useAtomRef, useAtomRefProp, useAtomRefPropValue - useAtomInitialValues - RegistryProvider and RegistryContext - Comprehensive test suite (264 lines)
1 parent 0149cf2 commit 57d3c70

21 files changed

Lines changed: 806 additions & 1 deletion

packages/atom-solid/.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
coverage/
2+
*.tsbuildinfo
3+
node_modules/
4+
.ultra.cache.json
5+
.DS_Store
6+
tmp/
7+
build/
8+
dist/
9+
.direnv/
10+
11+
# files
12+
/src/tsconfig.json
13+
/dist
14+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.js
2+
*.ts
3+

packages/atom-solid/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Changelog
2+

packages/atom-solid/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2023-present The Contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

packages/atom-solid/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# `@effect-atom/atom-solid`
2+
3+
SolidJS bindings for `@effect-atom/atom`.
4+

packages/atom-solid/docgen.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"exclude": ["src/internal/**/*.ts"],
3+
"theme": "mikearnaldi/just-the-docs",
4+
"parseCompilerOptions": {
5+
"noEmit": true,
6+
"strict": true,
7+
"target": "es2015",
8+
"lib": ["es2015"],
9+
"paths": {
10+
"@effect-atom/atom": ["./src/index.ts"],
11+
"@effect-atom/atom/*": ["./src/*"]
12+
}
13+
},
14+
"examplesCompilerOptions": {
15+
"noEmit": true,
16+
"strict": true,
17+
"target": "es2015",
18+
"lib": ["es2015"],
19+
"paths": {
20+
"@effect-atom/atom": ["./src/index.ts"],
21+
"@effect-atom/atom/*": ["./src/*"]
22+
}
23+
}
24+
}
25+

packages/atom-solid/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@effect-atom/atom-solid",
3+
"version": "0.4.5",
4+
"description": "Reactive toolkit for Effect",
5+
"type": "module",
6+
"publishConfig": {
7+
"access": "public",
8+
"directory": "dist"
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "https://github.com/tim-smart/effect-atom.git"
13+
},
14+
"homepage": "https://github.com/tim-smart/effect-atom",
15+
"scripts": {
16+
"build": "pnpm build-esm && pnpm build-cjs && pnpm build-annotate && build-utils pack-v2",
17+
"build-esm": "tsc -b tsconfig.build.json",
18+
"build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps",
19+
"build-annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps"
20+
},
21+
"keywords": [],
22+
"author": "Effect contributors",
23+
"license": "MIT",
24+
"sideEffects": [],
25+
"devDependencies": {
26+
"effect": "^3.19.0",
27+
"solid-js": "^1.9.0"
28+
},
29+
"peerDependencies": {
30+
"effect": "^3.19",
31+
"solid-js": ">=1 <2"
32+
},
33+
"dependencies": {
34+
"@effect-atom/atom": "workspace:^"
35+
}
36+
}
37+

packages/atom-solid/src/Hooks.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* @since 1.0.0
3+
*/
4+
import * as Atom from "@effect-atom/atom/Atom"
5+
import type * as AtomRef from "@effect-atom/atom/AtomRef"
6+
import * as Registry from "@effect-atom/atom/Registry"
7+
import type * as Result from "@effect-atom/atom/Result"
8+
import * as Cause from "effect/Cause"
9+
import * as Effect from "effect/Effect"
10+
import * as Exit from "effect/Exit"
11+
import { globalValue } from "effect/GlobalValue"
12+
import type { Accessor } from "solid-js"
13+
import { createSignal, onCleanup, useContext } from "solid-js"
14+
import { RegistryContext } from "./RegistryContext.js"
15+
16+
const initialValuesSet = globalValue(
17+
"@effect-atom/atom-solid/initialValuesSet",
18+
() => new WeakMap<Registry.Registry, WeakSet<Atom.Atom<any>>>()
19+
)
20+
21+
/**
22+
* @since 1.0.0
23+
* @category hooks
24+
*/
25+
export const useAtomInitialValues = (initialValues: Iterable<readonly [Atom.Atom<any>, any]>): void => {
26+
const registry = useContext(RegistryContext)
27+
let set = initialValuesSet.get(registry)
28+
if (set === undefined) {
29+
set = new WeakSet()
30+
initialValuesSet.set(registry, set)
31+
}
32+
for (const [atom, value] of initialValues) {
33+
if (!set.has(atom)) {
34+
set.add(atom)
35+
;(registry as any).ensureNode(atom).setValue(value)
36+
}
37+
}
38+
}
39+
40+
/**
41+
* @since 1.0.0
42+
* @category hooks
43+
*/
44+
export const useAtomValue: {
45+
<A>(atom: Atom.Atom<A>): Accessor<A>
46+
<A, B>(atom: Atom.Atom<A>, f: (_: A) => B): Accessor<B>
47+
} = <A>(atom: Atom.Atom<A>, f?: (_: A) => A): Accessor<A> => {
48+
const registry = useContext(RegistryContext)
49+
return createAtomAccessor(registry, f ? Atom.map(atom, f) : atom)
50+
}
51+
52+
function createAtomAccessor<A>(registry: Registry.Registry, atom: Atom.Atom<A>): Accessor<A> {
53+
const [value, setValue] = createSignal<A>(registry.get(atom))
54+
onCleanup(registry.subscribe(atom, setValue as any))
55+
return value
56+
}
57+
58+
function mountAtom<A>(registry: Registry.Registry, atom: Atom.Atom<A>): void {
59+
onCleanup(registry.mount(atom))
60+
}
61+
62+
function setAtom<R, W, Mode extends "value" | "promise" | "promiseExit" = never>(
63+
registry: Registry.Registry,
64+
atom: Atom.Writable<R, W>,
65+
options?: {
66+
readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : "value") | undefined
67+
}
68+
): "promise" extends Mode ? (
69+
(value: W) => Promise<Result.Result.Success<R>>
70+
) :
71+
"promiseExit" extends Mode ? (
72+
(value: W) => Promise<Exit.Exit<Result.Result.Success<R>, Result.Result.Failure<R>>>
73+
) :
74+
((value: W | ((value: R) => W)) => void)
75+
{
76+
if (options?.mode === "promise" || options?.mode === "promiseExit") {
77+
return ((value: W) => {
78+
registry.set(atom, value)
79+
const promise = Effect.runPromiseExit(
80+
Registry.getResult(registry, atom as Atom.Atom<Result.Result<any, any>>, { suspendOnWaiting: true })
81+
)
82+
return options!.mode === "promise" ? promise.then(flattenExit) : promise
83+
}) as any
84+
}
85+
return ((value: W | ((value: R) => W)) => {
86+
registry.set(atom, typeof value === "function" ? (value as any)(registry.get(atom)) : value)
87+
}) as any
88+
}
89+
90+
const flattenExit = <A, E>(exit: Exit.Exit<A, E>): A => {
91+
if (Exit.isSuccess(exit)) return exit.value
92+
throw Cause.squash(exit.cause)
93+
}
94+
95+
/**
96+
* @since 1.0.0
97+
* @category hooks
98+
*/
99+
export const useAtomMount = <A>(atom: Atom.Atom<A>): void => {
100+
const registry = useContext(RegistryContext)
101+
mountAtom(registry, atom)
102+
}
103+
104+
/**
105+
* @since 1.0.0
106+
* @category hooks
107+
*/
108+
export const useAtomSet = <
109+
R,
110+
W,
111+
Mode extends "value" | "promise" | "promiseExit" = never
112+
>(
113+
atom: Atom.Writable<R, W>,
114+
options?: {
115+
readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : "value") | undefined
116+
}
117+
): "promise" extends Mode ? (
118+
(value: W) => Promise<Result.Result.Success<R>>
119+
) :
120+
"promiseExit" extends Mode ? (
121+
(value: W) => Promise<Exit.Exit<Result.Result.Success<R>, Result.Result.Failure<R>>>
122+
) :
123+
((value: W | ((value: R) => W)) => void) =>
124+
{
125+
const registry = useContext(RegistryContext)
126+
mountAtom(registry, atom)
127+
return setAtom(registry, atom, options)
128+
}
129+
130+
/**
131+
* @since 1.0.0
132+
* @category hooks
133+
*/
134+
export const useAtomRefresh = <A>(atom: Atom.Atom<A>): () => void => {
135+
const registry = useContext(RegistryContext)
136+
mountAtom(registry, atom)
137+
return () => registry.refresh(atom)
138+
}
139+
140+
/**
141+
* @since 1.0.0
142+
* @category hooks
143+
*/
144+
export const useAtom = <R, W, const Mode extends "value" | "promise" | "promiseExit" = never>(
145+
atom: Atom.Writable<R, W>,
146+
options?: {
147+
readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : "value") | undefined
148+
}
149+
): readonly [
150+
value: Accessor<R>,
151+
write: "promise" extends Mode ? (
152+
(value: W) => Promise<Result.Result.Success<R>>
153+
) :
154+
"promiseExit" extends Mode ? (
155+
(value: W) => Promise<Exit.Exit<Result.Result.Success<R>, Result.Result.Failure<R>>>
156+
) :
157+
((value: W | ((value: R) => W)) => void)
158+
] => {
159+
const registry = useContext(RegistryContext)
160+
return [
161+
createAtomAccessor(registry, atom),
162+
setAtom(registry, atom, options)
163+
] as const
164+
}
165+
166+
/**
167+
* @since 1.0.0
168+
* @category hooks
169+
*/
170+
export const useAtomSubscribe = <A>(
171+
atom: Atom.Atom<A>,
172+
f: (_: A) => void,
173+
options?: { readonly immediate?: boolean }
174+
): void => {
175+
const registry = useContext(RegistryContext)
176+
onCleanup(registry.subscribe(atom, f, options))
177+
}
178+
179+
/**
180+
* @since 1.0.0
181+
* @category hooks
182+
*/
183+
export const useAtomRef = <A>(ref: AtomRef.ReadonlyRef<A>): Accessor<A> => {
184+
const [value, setValue] = createSignal(ref.value)
185+
onCleanup(ref.subscribe(setValue))
186+
return value
187+
}
188+
189+
/**
190+
* @since 1.0.0
191+
* @category hooks
192+
*/
193+
export const useAtomRefProp = <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): AtomRef.AtomRef<A[K]> =>
194+
ref.prop(prop)
195+
196+
/**
197+
* @since 1.0.0
198+
* @category hooks
199+
*/
200+
export const useAtomRefPropValue = <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): Accessor<A[K]> =>
201+
useAtomRef(useAtomRefProp(ref, prop))
202+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @since 1.0.0
3+
*/
4+
import type * as Atom from "@effect-atom/atom/Atom"
5+
import * as Registry from "@effect-atom/atom/Registry"
6+
import type { JSX } from "solid-js"
7+
import { createComponent, createContext, onCleanup } from "solid-js"
8+
9+
/**
10+
* @since 1.0.0
11+
* @category context
12+
*/
13+
export const RegistryContext = createContext<Registry.Registry>(Registry.make())
14+
15+
/**
16+
* @since 1.0.0
17+
* @category context
18+
*/
19+
export const RegistryProvider = (options: {
20+
readonly children?: JSX.Element | undefined
21+
readonly initialValues?: Iterable<readonly [Atom.Atom<any>, any]> | undefined
22+
readonly scheduleTask?: ((f: () => void) => void) | undefined
23+
readonly timeoutResolution?: number | undefined
24+
readonly defaultIdleTTL?: number | undefined
25+
}) => {
26+
const registry = Registry.make({
27+
scheduleTask: options.scheduleTask,
28+
initialValues: options.initialValues,
29+
timeoutResolution: options.timeoutResolution,
30+
defaultIdleTTL: options.defaultIdleTTL
31+
})
32+
onCleanup(() => registry.dispose())
33+
return createComponent(RegistryContext.Provider, {
34+
value: registry,
35+
get children() {
36+
return options.children
37+
}
38+
})
39+
}
40+

0 commit comments

Comments
 (0)