Compare commits
19 Commits
b3ec1c4f49
...
ec264e0381
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec264e0381 | ||
|
|
18d94c77e2 | ||
|
|
4f091ae221 | ||
|
|
671a80b6ff | ||
|
|
249de93047 | ||
|
|
ae6bb410a3 | ||
|
|
2aaee4826b | ||
|
|
f50adbf119 | ||
|
|
cf0951039c | ||
|
|
4b6cf9a46e | ||
|
|
12849d37da | ||
|
|
c60c396054 | ||
|
|
1720266761 | ||
|
|
edec837a87 | ||
|
|
d8553e95e2 | ||
|
|
f6dc7a0722 | ||
|
|
ed85f9804c | ||
|
|
cd2df017ec | ||
|
|
79a3779005 |
8
packages/example/src/domain/Post.ts
Normal file
8
packages/example/src/domain/Post.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class Post extends Schema.Class<Post>("Post")({
|
||||||
|
id: Schema.String,
|
||||||
|
title: Schema.String,
|
||||||
|
content: Schema.String,
|
||||||
|
}) {}
|
||||||
1
packages/example/src/domain/index.ts
Normal file
1
packages/example/src/domain/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as Post from "./Post"
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||||
|
import { Layer } from "effect"
|
||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { routeTree } from './routeTree.gen'
|
import { Reffuse } from "./reffuse"
|
||||||
|
import { routeTree } from "./routeTree.gen"
|
||||||
|
import { FetchData } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
const layer = Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(FetchData.mockLayer)
|
||||||
|
)
|
||||||
|
|
||||||
const router = createRouter({ routeTree })
|
const router = createRouter({ routeTree })
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
@@ -13,8 +20,11 @@ declare module "@tanstack/react-router" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<RouterProvider router={router} />
|
<Reffuse.Provider layer={layer}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</Reffuse.Provider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { make } from "@thilawyn/reffuse/Reffuse"
|
import { make } from "@thilawyn/reffuse/Reffuse"
|
||||||
import { Layer } from "effect"
|
import { FetchData } from "./services"
|
||||||
|
|
||||||
|
|
||||||
export const Reffuse = make(Layer.empty)
|
export const Reffuse = make<FetchData.FetchData>()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Reffuse } from "@/reffuse"
|
|
||||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
||||||
|
|
||||||
@@ -8,16 +7,14 @@ export const Route = createRootRoute({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function Root() {
|
function Root() {
|
||||||
return (
|
return <>
|
||||||
<Reffuse.Provider>
|
<div className="container flex-row gap-2 justify-center items-center mx-auto mb-4">
|
||||||
<div className="container mx-auto flex-row justify-center items-center gap-2 mb-4">
|
<Link to="/">Index</Link>
|
||||||
<Link to="/">Index</Link>
|
<Link to="/time">Time</Link>
|
||||||
<Link to="/time">Time</Link>
|
<Link to="/count">Count</Link>
|
||||||
<Link to="/count">Count</Link>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</Reffuse.Provider>
|
</>
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const Route = createFileRoute("/count")({
|
|||||||
|
|
||||||
function Count() {
|
function Count() {
|
||||||
|
|
||||||
const runtime = Reffuse.useRuntime()
|
const runSync = Reffuse.useRunSync()
|
||||||
|
|
||||||
const countRef = Reffuse.useRef(0)
|
const countRef = Reffuse.useRef(0)
|
||||||
const [count] = Reffuse.useRefState(countRef)
|
const [count] = Reffuse.useRefState(countRef)
|
||||||
@@ -18,7 +18,7 @@ function Count() {
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
||||||
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runtime.runSync)}>
|
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
|
||||||
count is {count}
|
count is {count}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
import { Reffuse } from "@/reffuse"
|
||||||
|
import { FetchData } from "@/services"
|
||||||
|
import { Reffuse as PostsReffuse } from "@/views/posts/reffuse"
|
||||||
|
import { PostsState } from "@/views/posts/services"
|
||||||
|
import { VPosts } from "@/views/posts/VPosts"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
@@ -6,5 +12,19 @@ export const Route = createFileRoute("/")({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function Index() {
|
function Index() {
|
||||||
return <></>
|
|
||||||
|
const postsLayer = Reffuse.useMemo(FetchData.FetchData.pipe(
|
||||||
|
Effect.flatMap(({ fetchPosts }) => fetchPosts),
|
||||||
|
Effect.map(PostsState.make),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto">
|
||||||
|
<PostsReffuse.Provider layer={postsLayer}>
|
||||||
|
<VPosts />
|
||||||
|
</PostsReffuse.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
packages/example/src/services/FetchData.ts
Normal file
24
packages/example/src/services/FetchData.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Post } from "@/domain"
|
||||||
|
import { Chunk, Context, Effect, Layer } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class FetchData extends Context.Tag("FetchData")<FetchData, {
|
||||||
|
readonly fetchPosts: Effect.Effect<Chunk.Chunk<Post.Post>>
|
||||||
|
}>() {}
|
||||||
|
|
||||||
|
|
||||||
|
export const mockLayer = Layer.succeed(FetchData, {
|
||||||
|
fetchPosts: Effect.succeed(Chunk.make(
|
||||||
|
Post.Post.make({
|
||||||
|
id: "1",
|
||||||
|
title: "Lorem ipsum dolor sit amet",
|
||||||
|
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget lacus sit amet diam suscipit porttitor non at felis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla risus ligula, elementum nec scelerisque eget, volutpat vel sapien. Phasellus aliquam ac neque vitae sodales. Nunc sodales congue odio. Nulla eget nisl cursus, convallis lorem at, varius lectus. Aliquam vitae mauris vel mi dignissim condimentum. Proin sed dignissim sapien, ut cursus ex. Donec eget sapien sagittis, auctor metus vitae, fringilla lacus. Donec ut elit a quam aliquet consectetur interdum eu nisl. Etiam nec convallis purus, eu venenatis nulla. Phasellus non metus id mauris tincidunt cursus. Cras varius aliquet diam eu blandit. In hac habitasse platea dictumst.",
|
||||||
|
}),
|
||||||
|
|
||||||
|
Post.Post.make({
|
||||||
|
id: "2",
|
||||||
|
title: "Vestibulum non bibendum ligula",
|
||||||
|
content: "Vestibulum non bibendum ligula. Integer pellentesque, diam ac faucibus volutpat, nulla libero porttitor nunc, ac pulvinar tortor diam id ipsum. Sed id enim at odio euismod imperdiet et ac purus. Etiam tempus ipsum semper scelerisque mollis. Integer auctor, magna et tristique tempus, nisi mi euismod est, nec finibus quam nunc nec libero. Maecenas aliquet viverra magna, vitae blandit ligula pharetra id. Vestibulum vel lacus at nibh placerat tincidunt. Sed suscipit tellus vel felis euismod, et sollicitudin neque cursus. Curabitur dapibus eros vitae ligula suscipit, at facilisis risus venenatis. Sed pharetra blandit pulvinar. Vivamus vestibulum at ligula pulvinar fringilla. Suspendisse vel mattis libero, eget vulputate massa. Vivamus vehicula, lectus id tempor maximus, erat tortor blandit purus, at scelerisque nunc urna faucibus sapien.",
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
})
|
||||||
1
packages/example/src/services/index.ts
Normal file
1
packages/example/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as FetchData from "./FetchData"
|
||||||
34
packages/example/src/views/post/VPost.tsx
Normal file
34
packages/example/src/views/post/VPost.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Post } from "@/domain"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { PostsState } from "../posts/services"
|
||||||
|
import { Reffuse } from "./reffuse"
|
||||||
|
|
||||||
|
|
||||||
|
export interface VPostProps {
|
||||||
|
readonly index: number
|
||||||
|
readonly post: Post.Post
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function VPost({ post, index }: VPostProps) {
|
||||||
|
|
||||||
|
const runSync = Reffuse.useRunSync()
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-col gap-1 items-stretch">
|
||||||
|
<p>{post.title}</p>
|
||||||
|
<p>{post.content}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => PostsState.PostsState.pipe(
|
||||||
|
Effect.flatMap(state => state.remove(index)),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
1
packages/example/src/views/post/reffuse.ts
Normal file
1
packages/example/src/views/post/reffuse.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Reffuse } from "../posts/reffuse"
|
||||||
25
packages/example/src/views/posts/VPosts.tsx
Normal file
25
packages/example/src/views/posts/VPosts.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Chunk } from "effect"
|
||||||
|
import { VPost } from "../post/VPost"
|
||||||
|
import { Reffuse } from "./reffuse"
|
||||||
|
import { PostsState } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export function VPosts() {
|
||||||
|
|
||||||
|
const state = Reffuse.useMemo(PostsState.PostsState)
|
||||||
|
const [posts] = Reffuse.useRefState(state.posts)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-col gap-2 items-stretch">
|
||||||
|
{Chunk.map(posts, (post, index) => (
|
||||||
|
<VPost
|
||||||
|
key={`${ index }-${ post.id }`}
|
||||||
|
index={index}
|
||||||
|
post={post}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
5
packages/example/src/views/posts/reffuse.ts
Normal file
5
packages/example/src/views/posts/reffuse.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Reffuse as RootReffuse } from "@/reffuse"
|
||||||
|
import { PostsState } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export const Reffuse = RootReffuse.extend<PostsState.PostsState>()
|
||||||
15
packages/example/src/views/posts/services/PostsState.ts
Normal file
15
packages/example/src/views/posts/services/PostsState.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Post } from "@/domain"
|
||||||
|
import { Chunk, Context, Effect, Layer, Ref, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class PostsState extends Context.Tag("PostsState")<PostsState, {
|
||||||
|
readonly posts: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Post.Post>>
|
||||||
|
readonly remove: (index: number) => Effect.Effect<void>
|
||||||
|
}>() {}
|
||||||
|
|
||||||
|
export const make = (posts: Chunk.Chunk<Post.Post>) => Layer.effect(PostsState, SubscriptionRef.make(posts).pipe(
|
||||||
|
Effect.map(posts => ({
|
||||||
|
posts,
|
||||||
|
remove: (index: number) => Ref.update(posts, Chunk.remove(index)),
|
||||||
|
}))
|
||||||
|
))
|
||||||
1
packages/example/src/views/posts/services/index.ts
Normal file
1
packages/example/src/views/posts/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as PostsState from "./PostsState"
|
||||||
182
packages/reffuse/src/Reffuse.ts
Normal file
182
packages/reffuse/src/Reffuse.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { Context, Effect, Fiber, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import React from "react"
|
||||||
|
import * as ReffuseReactContext from "./ReffuseReactContext.js"
|
||||||
|
|
||||||
|
|
||||||
|
export class Reffuse<
|
||||||
|
RuntimeR,
|
||||||
|
ContextR extends ParentContextR | OwnContextR,
|
||||||
|
OwnContextR,
|
||||||
|
ParentContextR = never,
|
||||||
|
> {
|
||||||
|
|
||||||
|
readonly Context = React.createContext<ReffuseReactContext.Value<RuntimeR, ContextR>>(null!)
|
||||||
|
readonly Provider: ReffuseReactContext.Provider<RuntimeR, OwnContextR, ParentContextR>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly runtime: Runtime.Runtime<RuntimeR>,
|
||||||
|
parent?: Reffuse<RuntimeR, ParentContextR, unknown, unknown>,
|
||||||
|
) {
|
||||||
|
this.Provider = parent
|
||||||
|
? ReffuseReactContext.makeNestedProvider(runtime, this.Context, parent)
|
||||||
|
: ReffuseReactContext.makeRootProvider(runtime, this.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
extend<OwnContextR = never>() {
|
||||||
|
return new Reffuse<
|
||||||
|
RuntimeR,
|
||||||
|
ContextR | OwnContextR,
|
||||||
|
OwnContextR,
|
||||||
|
ContextR
|
||||||
|
>(this.runtime, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useRuntime(): Runtime.Runtime<RuntimeR> {
|
||||||
|
return React.useContext(this.Context).runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
useContext(): Context.Context<ContextR> {
|
||||||
|
return React.useContext(this.Context).context
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useRunSync() {
|
||||||
|
const { runtime, context } = React.useContext(this.Context)
|
||||||
|
|
||||||
|
return React.useCallback(<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, RuntimeR | ContextR>
|
||||||
|
): A => effect.pipe(
|
||||||
|
Effect.provide(context),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
), [runtime, context])
|
||||||
|
}
|
||||||
|
|
||||||
|
useRunPromise() {
|
||||||
|
const { runtime, context } = React.useContext(this.Context)
|
||||||
|
|
||||||
|
return React.useCallback(<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, RuntimeR | ContextR>,
|
||||||
|
options?: { readonly signal?: AbortSignal },
|
||||||
|
): Promise<A> => effect.pipe(
|
||||||
|
Effect.provide(context),
|
||||||
|
effect => Runtime.runPromise(runtime)(effect, options),
|
||||||
|
), [runtime, context])
|
||||||
|
}
|
||||||
|
|
||||||
|
useRunFork() {
|
||||||
|
const { runtime, context } = React.useContext(this.Context)
|
||||||
|
|
||||||
|
return React.useCallback(<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, RuntimeR | ContextR>,
|
||||||
|
options?: Runtime.RunForkOptions,
|
||||||
|
): Fiber.RuntimeFiber<A, E> => effect.pipe(
|
||||||
|
Effect.provide(context),
|
||||||
|
effect => Runtime.runFork(runtime)(effect, options),
|
||||||
|
), [runtime, context])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useMemo<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, RuntimeR | ContextR>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: RenderOptions,
|
||||||
|
): A {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
|
return React.useMemo(() => runSync(effect), [
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
|
...(deps ?? []),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// useEffect<A, E>(
|
||||||
|
// effect: Effect.Effect<A, E, RuntimeR | ContextR | Scope.Scope>,
|
||||||
|
// deps?: React.DependencyList,
|
||||||
|
// options?: RenderOptions,
|
||||||
|
// ): void {
|
||||||
|
// const runSync = this.useRunSync()
|
||||||
|
|
||||||
|
// return React.useEffect(() => { runSync(effect) }, [
|
||||||
|
// ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
|
// ...(deps ?? []),
|
||||||
|
// ])
|
||||||
|
// }
|
||||||
|
|
||||||
|
useSuspense<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, RuntimeR | ContextR>,
|
||||||
|
options?: { readonly signal?: AbortSignal },
|
||||||
|
): A {
|
||||||
|
const runPromise = this.useRunPromise()
|
||||||
|
return React.use(runPromise(effect, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
useFork<A, E>(
|
||||||
|
effect: Effect.Effect<A, E, RuntimeR | ContextR | Scope.Scope>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions & RenderOptions,
|
||||||
|
): void {
|
||||||
|
const runFork = this.useRunFork()
|
||||||
|
|
||||||
|
return React.useEffect(() => {
|
||||||
|
const fiber = runFork(Effect.scoped(effect), options)
|
||||||
|
return () => { runFork(Fiber.interrupt(fiber)) }
|
||||||
|
}, [
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runFork],
|
||||||
|
...(deps ?? []),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useRef<A>(value: A): SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
return this.useMemo(
|
||||||
|
SubscriptionRef.make(value),
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: false }, // Do not recreate the ref when the context changes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useRefFromEffect<A, E>(effect: Effect.Effect<A, E, RuntimeR | ContextR>): SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
return this.useMemo(
|
||||||
|
effect.pipe(Effect.flatMap(SubscriptionRef.make)),
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: false }, // Do not recreate the ref when the context changes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useRefState<A>(ref: SubscriptionRef.SubscriptionRef<A>): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
|
const initialState = React.useMemo(() => runSync(ref), [ref])
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||||
|
|
||||||
|
this.useFork(Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
||||||
|
setReactStateValue(v)
|
||||||
|
)), [ref])
|
||||||
|
|
||||||
|
const setValue = React.useCallback((setStateAction: React.SetStateAction<A>) =>
|
||||||
|
runSync(Ref.update(ref, previousState =>
|
||||||
|
typeof setStateAction === "function"
|
||||||
|
? (setStateAction as (prevState: A) => A)(previousState)
|
||||||
|
: setStateAction
|
||||||
|
)),
|
||||||
|
[ref])
|
||||||
|
|
||||||
|
return [reactStateValue, setValue]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface RenderOptions {
|
||||||
|
/** Prevents re-executing the effect when the Effect runtime or context changes. Defaults to `false`. */
|
||||||
|
readonly doNotReExecuteOnRuntimeOrContextChange?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const make = <R = never>(): Reffuse<never, R, R> =>
|
||||||
|
new Reffuse(Runtime.defaultRuntime)
|
||||||
|
|
||||||
|
export const makeWithRuntime = <R = never>() =>
|
||||||
|
<RuntimeR>(runtime: Runtime.Runtime<RuntimeR>): Reffuse<RuntimeR, R, R> =>
|
||||||
|
new Reffuse(runtime)
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { Effect, Fiber, Layer, ManagedRuntime, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export class Reffuse<R, ER> {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
|
||||||
) {
|
|
||||||
this.Context = React.createContext<ManagedRuntime.ManagedRuntime<R, ER>>(null!)
|
|
||||||
|
|
||||||
this.Provider = (props: { readonly children?: React.ReactNode }) => (
|
|
||||||
<this.Context
|
|
||||||
{...props}
|
|
||||||
value={runtime}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
const context = runtime.runtimeEffect.pipe(
|
|
||||||
Effect.map(r => Layer.succeedContext(r.context)),
|
|
||||||
runtime.runSync,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly Context: React.Context<ManagedRuntime.ManagedRuntime<R, ER>>
|
|
||||||
readonly Provider: React.FC<{ readonly children?: React.ReactNode }>
|
|
||||||
|
|
||||||
|
|
||||||
useRuntime(): ManagedRuntime.ManagedRuntime<R, ER> {
|
|
||||||
return React.useContext(this.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
useFork<A, E>(
|
|
||||||
self: Effect.Effect<A, E, R | Scope.Scope>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions,
|
|
||||||
): void {
|
|
||||||
const runtime = this.useRuntime()
|
|
||||||
|
|
||||||
return React.useEffect(() => {
|
|
||||||
const fiber = runtime.runFork(Effect.scoped(self), options)
|
|
||||||
return () => { runtime.runFork(Fiber.interrupt(fiber)) }
|
|
||||||
}, [runtime, ...deps ?? []])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
useRef<A>(value: A): SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
const runtime = this.useRuntime()
|
|
||||||
return React.useMemo(() => runtime.runSync(SubscriptionRef.make(value)), [])
|
|
||||||
}
|
|
||||||
|
|
||||||
useRefFromEffect<A, E>(effect: Effect.Effect<A, E, R>): SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
const runtime = this.useRuntime()
|
|
||||||
|
|
||||||
return React.useMemo(() => runtime.runSync(effect.pipe(
|
|
||||||
Effect.flatMap(SubscriptionRef.make)
|
|
||||||
)), [])
|
|
||||||
}
|
|
||||||
|
|
||||||
useRefState<A>(ref: SubscriptionRef.SubscriptionRef<A>): [A, React.Dispatch<React.SetStateAction<A>>] {
|
|
||||||
const runtime = this.useRuntime()
|
|
||||||
|
|
||||||
const initialState = React.useMemo(() => runtime.runSync(ref), [ref])
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
|
||||||
|
|
||||||
this.useFork(Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
|
||||||
setReactStateValue(v)
|
|
||||||
)), [ref])
|
|
||||||
|
|
||||||
const setValue = React.useCallback((setStateAction: React.SetStateAction<A>) =>
|
|
||||||
runtime.runSync(Ref.update(ref, previousState =>
|
|
||||||
typeof setStateAction === "function"
|
|
||||||
? (setStateAction as (prevState: A) => A)(previousState)
|
|
||||||
: setStateAction
|
|
||||||
)),
|
|
||||||
[ref])
|
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const make = <ROut, E>(layer: Layer.Layer<ROut, E, never>): Reffuse<ROut, E> =>
|
|
||||||
new Reffuse(ManagedRuntime.make(layer))
|
|
||||||
84
packages/reffuse/src/ReffuseReactContext.tsx
Normal file
84
packages/reffuse/src/ReffuseReactContext.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Context, Effect, Runtime, type Layer } from "effect"
|
||||||
|
import React from "react"
|
||||||
|
import type * as Reffuse from "./Reffuse.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface Value<RuntimeR, ContextR> {
|
||||||
|
readonly runtime: Runtime.Runtime<RuntimeR>
|
||||||
|
readonly context: Context.Context<ContextR>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Provider<
|
||||||
|
RuntimeR,
|
||||||
|
OwnContextR,
|
||||||
|
ParentContextR,
|
||||||
|
> = React.FC<ProviderProps<RuntimeR, OwnContextR, ParentContextR>>
|
||||||
|
|
||||||
|
export interface ProviderProps<
|
||||||
|
RuntimeR,
|
||||||
|
OwnContextR,
|
||||||
|
ParentContextR,
|
||||||
|
> {
|
||||||
|
readonly layer: Layer.Layer<OwnContextR, unknown, RuntimeR | ParentContextR>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeRootProvider<
|
||||||
|
RuntimeR,
|
||||||
|
ContextR extends ParentContextR | OwnContextR,
|
||||||
|
OwnContextR,
|
||||||
|
ParentContextR,
|
||||||
|
>(
|
||||||
|
runtime: Runtime.Runtime<RuntimeR>,
|
||||||
|
ReactContext: React.Context<Value<RuntimeR, ContextR>>,
|
||||||
|
): Provider<RuntimeR, OwnContextR, ParentContextR> {
|
||||||
|
return function ReffuseRootReactContextProvider(props) {
|
||||||
|
const value = React.useMemo(() => ({
|
||||||
|
runtime,
|
||||||
|
context: Effect.context<ContextR>().pipe(
|
||||||
|
Effect.provide(props.layer),
|
||||||
|
Effect.provide(Context.empty() as Context.Context<ParentContextR>), // Just there for type safety. ParentContextR is always never here anyway
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
),
|
||||||
|
}), [props.layer])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactContext
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeNestedProvider<
|
||||||
|
RuntimeR,
|
||||||
|
ContextR extends ParentContextR | OwnContextR,
|
||||||
|
OwnContextR,
|
||||||
|
ParentContextR,
|
||||||
|
>(
|
||||||
|
runtime: Runtime.Runtime<RuntimeR>,
|
||||||
|
ReactContext: React.Context<Value<RuntimeR, ContextR>>,
|
||||||
|
parent: Reffuse.Reffuse<RuntimeR, ParentContextR, unknown, unknown>,
|
||||||
|
): Provider<RuntimeR, OwnContextR, ParentContextR> {
|
||||||
|
return function ReffuseNestedReactContextProvider(props) {
|
||||||
|
const parentContext = parent.useContext()
|
||||||
|
|
||||||
|
const value = React.useMemo(() => ({
|
||||||
|
runtime,
|
||||||
|
context: Effect.context<ContextR>().pipe(
|
||||||
|
Effect.provide(props.layer),
|
||||||
|
Effect.provide(parentContext),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
),
|
||||||
|
}), [props.layer, parentContext])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactContext
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * as Reffuse from "./Reffuse.js"
|
export * as Reffuse from "./Reffuse.js"
|
||||||
|
export * as ReffuseReactContext from "./ReffuseReactContext.js"
|
||||||
|
|||||||
43
packages/reffuse/src/tests.ts
Normal file
43
packages/reffuse/src/tests.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Context, Effect, FiberRefs, Layer, Ref, Runtime, RuntimeFlags } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
const runtime = Runtime.make({
|
||||||
|
context: Context.empty(),
|
||||||
|
runtimeFlags: RuntimeFlags.make(),
|
||||||
|
fiberRefs: FiberRefs.empty(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const createRunSync = <R1, R2>(runtime: Runtime.Runtime<R1>, layer: Layer.Layer<R2>) => {
|
||||||
|
const context = Effect.context<R1 | R2>().pipe(
|
||||||
|
Effect.provide(layer),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
)
|
||||||
|
|
||||||
|
return <A, E>(effect: Effect.Effect<A, E, R1 | R2>) =>
|
||||||
|
Runtime.runSync(runtime)(effect.pipe(Effect.provide(context)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MyService extends Effect.Service<MyService>()("MyServer", {
|
||||||
|
effect: Effect.gen(function*() {
|
||||||
|
return {
|
||||||
|
ref: yield* Ref.make("initial value")
|
||||||
|
} as const
|
||||||
|
})
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
const MyLayer = Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(MyService.Default)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const runSync = createRunSync(runtime, MyLayer)
|
||||||
|
|
||||||
|
const setMyServiceValue = (value: string) => Effect.gen(function*() {
|
||||||
|
console.log("previous value: ", yield* (yield* MyService).ref)
|
||||||
|
yield* Ref.set((yield* MyService).ref, value)
|
||||||
|
console.log("new value: ", yield* (yield* MyService).ref)
|
||||||
|
})
|
||||||
|
|
||||||
|
runSync(setMyServiceValue("1"))
|
||||||
|
runSync(setMyServiceValue("2"))
|
||||||
Reference in New Issue
Block a user