297 lines
8.9 KiB
TypeScript
297 lines
8.9 KiB
TypeScript
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<number, Error>())
|
|
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<typeof useAtomResource>[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<A>(ref: AtomRef.ReadonlyRef<A>, onValue: (_: A) => void) {
|
|
return createRoot((dispose) => {
|
|
const accessor = useAtomRef(() => ref)
|
|
createEffect(() => {
|
|
onValue(accessor())
|
|
})
|
|
return dispose
|
|
})
|
|
}
|
|
|
|
const renderAccessor = function<A>(makeAccessor: () => Accessor<A>, onValue: (_: A) => void) {
|
|
return createRoot((dispose) => {
|
|
const accessor = makeAccessor()
|
|
createEffect(() => {
|
|
onValue(accessor())
|
|
})
|
|
return dispose
|
|
})
|
|
}
|
|
|
|
const renderAtomValue = function<A, B = A>(
|
|
atom: Atom.Atom<A>,
|
|
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<A, E>(
|
|
atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>,
|
|
options?: {
|
|
readonly suspendOnWaiting?: boolean | undefined
|
|
}
|
|
) {
|
|
let resource:
|
|
| Resource<A>
|
|
| undefined
|
|
const dispose = createRoot((dispose) => {
|
|
;[resource] = useAtomResource(atom, options)
|
|
createEffect(() => {
|
|
resource?.()
|
|
})
|
|
return dispose
|
|
})
|
|
return { resource: resource!, dispose }
|
|
}
|