25 Commits

Author SHA1 Message Date
Julien Valverdé
6526953c37 useSuspense
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-16 00:43:28 +01:00
Julien Valverdé
4d411bc8dc useFork refactoring
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-16 00:02:56 +01:00
Julien Valverdé
0e7f5d93bb Fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 23:54:38 +01:00
Julien Valverdé
ca961e9122 useLayoutEffect
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 23:44:36 +01:00
Julien Valverdé
e340ab0c8e Scope options
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 23:40:56 +01:00
Julien Valverdé
2adc6ed186 Scope fix
Some checks failed
Lint / lint (push) Failing after 11s
2025-01-15 23:30:02 +01:00
Julien Valverdé
9ff34addcd useEffect scope
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 23:22:43 +01:00
Julien Valverdé
010416f0b1 useEffect
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 22:12:33 +01:00
Julien Valverdé
14e028e8c8 Todo fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 22:05:21 +01:00
Julien Valverdé
53ed31be8d Todo work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 21:46:13 +01:00
Julien Valverdé
b46ba311ae Fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 21:04:06 +01:00
Julien Valverdé
8e6adc6b85 Todo work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 20:59:07 +01:00
Julien Valverdé
3813e63982 Todo work
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 20:09:00 +01:00
Julien Valverdé
f3f44d9abe Refactoring
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 18:07:40 +01:00
Julien Valverdé
9acf34ee4a Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 17:54:56 +01:00
Julien Valverdé
4a525d5f5d Example refactoring
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 17:52:28 +01:00
Julien Valverdé
8b5c6169da Cleanup
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 17:42:37 +01:00
Julien Valverdé
e142010128 useMergeAllLayers
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 17:40:50 +01:00
Julien Valverdé
3cc34a2ed1 Working Reffuse
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 16:41:23 +01:00
Julien Valverdé
15c1fdd54c useMergeAll
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 02:06:46 +01:00
Julien Valverdé
cb798ad466 Tests
Some checks failed
Lint / lint (push) Failing after 10s
2025-01-15 01:54:27 +01:00
Julien Valverdé
75c3ad31d0 Fix
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 01:32:18 +01:00
Julien Valverdé
102e8a12b6 ReffuseContext
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 01:09:22 +01:00
Julien Valverdé
52b7b071f4 Tests
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 00:37:00 +01:00
Julien Valverdé
9f08894b61 ReffuseContext
Some checks failed
Lint / lint (push) Failing after 9s
2025-01-15 00:16:57 +01:00
22 changed files with 486 additions and 223 deletions

BIN
bun.lockb

Binary file not shown.

2
bunfig.toml Normal file
View File

@@ -0,0 +1,2 @@
[install.scopes]
"@thilawyn" = "https://git.valverde.cloud/api/packages/thilawyn/npm/"

View File

@@ -14,6 +14,7 @@
"@tanstack/react-router": "^1.95.3", "@tanstack/react-router": "^1.95.3",
"@tanstack/router-devtools": "^1.95.3", "@tanstack/router-devtools": "^1.95.3",
"@tanstack/router-plugin": "^1.95.3", "@tanstack/router-plugin": "^1.95.3",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.0.4", "@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2", "@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
@@ -30,5 +31,13 @@
"typescript": "~5.6.2", "typescript": "~5.6.2",
"typescript-eslint": "^8.18.2", "typescript-eslint": "^8.18.2",
"vite": "^6.0.5" "vite": "^6.0.5"
},
"dependencies": {
"@effect/platform": "^0.73.1",
"@effect/platform-browser": "^0.52.1",
"@radix-ui/themes": "^3.1.6",
"@typed/id": "^0.17.1",
"lucide-react": "^0.471.1",
"mobx": "^6.13.5"
} }
} }

View File

@@ -0,0 +1,26 @@
import { ThSchema } from "@thilawyn/thilaschema"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect, Schema } from "effect"
export class Todo extends Schema.Class<Todo>("Todo")({
_tag: Schema.tag("Todo"),
id: Schema.String,
content: Schema.String,
completedAt: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
}) {}
export const TodoFromJsonStruct = Schema.Struct({
...Todo.fields,
completedAt: Schema.Option(Schema.DateTimeUtc),
}).pipe(
ThSchema.assertEncodedJsonifiable
)
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
export const generateUniqueID = makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom)
)

View File

@@ -1 +1,2 @@
export * as Post from "./Post" export * as Post from "./Post"
export * as Todo from "./Todo"

View File

@@ -1,9 +1,9 @@
import { createRouter, RouterProvider } from "@tanstack/react-router" import { createRouter, RouterProvider } from "@tanstack/react-router"
import { ReffuseRuntime } from "@thilawyn/reffuse"
import { Layer } from "effect" 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 { GlobalContext } from "./reffuse"
import { Reffuse } from "./reffuse"
import { routeTree } from "./routeTree.gen" import { routeTree } from "./routeTree.gen"
import { FetchData } from "./services" import { FetchData } from "./services"
@@ -23,8 +23,10 @@ declare module "@tanstack/react-router" {
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<Reffuse.Provider layer={layer}> <ReffuseRuntime.Provider>
<GlobalContext.Provider layer={layer}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</Reffuse.Provider> </GlobalContext.Provider>
</ReffuseRuntime.Provider>
</StrictMode> </StrictMode>
) )

View File

@@ -1,5 +1,7 @@
import { ReffuseContext } from "@thilawyn/reffuse"
import { make } from "@thilawyn/reffuse/Reffuse" import { make } from "@thilawyn/reffuse/Reffuse"
import { FetchData } from "./services" import { FetchData } from "./services"
export const Reffuse = make<FetchData.FetchData>() export const GlobalContext = ReffuseContext.make<FetchData.FetchData>()
export const Reffuse = make(GlobalContext)

View File

@@ -1,5 +1,8 @@
import { Container, Flex, Theme } from "@radix-ui/themes"
import "@radix-ui/themes/styles.css"
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"
import "../index.css"
export const Route = createRootRoute({ export const Route = createRootRoute({
@@ -7,14 +10,18 @@ export const Route = createRootRoute({
}) })
function Root() { function Root() {
return <> return (
<div className="container flex-row gap-2 justify-center items-center mx-auto mb-4"> <Theme>
<Container>
<Flex direction="row" justify="center" align="center" gap="2">
<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> </Flex>
</Container>
<Outlet /> <Outlet />
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</> </Theme>
)
} }

View File

@@ -1,10 +1,11 @@
import { Reffuse } from "@/reffuse" import { Reffuse } from "@/reffuse"
import { FetchData } from "@/services" import { TodosContext } from "@/todos/reffuse"
import { Reffuse as PostsReffuse } from "@/views/posts/reffuse" import { TodosState } from "@/todos/services"
import { PostsState } from "@/views/posts/services" import { VTodos } from "@/todos/views/VTodos"
import { VPosts } from "@/views/posts/VPosts" import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Effect } from "effect" import { Console, Effect, Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
@@ -13,18 +14,21 @@ export const Route = createFileRoute("/")({
function Index() { function Index() {
const postsLayer = Reffuse.useMemo(FetchData.FetchData.pipe( const todosLayer = useMemo(() => Layer.empty.pipe(
Effect.flatMap(({ fetchPosts }) => fetchPosts), Layer.provideMerge(TodosState.make("todos"))
Effect.map(PostsState.make), ), [])
Reffuse.useEffect(Effect.addFinalizer(() => Console.log("Effect cleanup")).pipe(
Effect.flatMap(() => Console.log("Effect recalculated"))
)) ))
return ( return (
<div className="container mx-auto"> <Container>
<PostsReffuse.Provider layer={postsLayer}> <TodosContext.Provider layer={todosLayer}>
<VPosts /> <VTodos />
</PostsReffuse.Provider> </TodosContext.Provider>
</div> </Container>
) )
} }

View File

@@ -0,0 +1,8 @@
import { GlobalContext } from "@/reffuse"
import { ReffuseContext } from "@thilawyn/reffuse"
import { make } from "@thilawyn/reffuse/Reffuse"
import { TodosState } from "./services"
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
export const Reffuse = make(GlobalContext, TodosContext)

View File

@@ -0,0 +1,69 @@
import { Todo } from "@/domain"
import { KeyValueStore } from "@effect/platform"
import { BrowserKeyValueStore } from "@effect/platform-browser"
import { PlatformError } from "@effect/platform/Error"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
export class TodosState extends Context.Tag("TodosState")<TodosState, {
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
readonly remove: (index: number) => Effect.Effect<void>
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
}>() {}
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => kv.get(key)),
Effect.flatMap(identity),
Effect.flatMap(Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.decode,
)),
Effect.flatMap(v => Ref.set(todos, v)),
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
Effect.flatMap(([kv, values]) => values.pipe(
Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.encode,
),
Effect.flatMap(v => kv.set(key, v)),
)),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
// const moveUp = (index: number) => Effect.gen(function*() {
// })
yield* readFromLocalStorage
return {
todos,
readFromLocalStorage,
saveToLocalStorage,
prepend,
replace,
remove,
}
}))

View File

@@ -0,0 +1 @@
export * as TodosState from "./TodosState"

View File

@@ -0,0 +1,52 @@
import { Todo } from "@/domain"
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
import { Effect, Option } from "effect"
import { Reffuse } from "../reffuse"
import { TodosState } from "../services"
export function VNewTodo() {
const runSync = Reffuse.useRunSync()
const createEmptyTodo = Todo.generateUniqueID.pipe(
Effect.map(id => Todo.Todo.make({
id,
content: "",
completedAt: Option.none(),
}, true))
)
const todoRef = Reffuse.useRefFromEffect(createEmptyTodo)
const [todo, setTodo] = Reffuse.useRefState(todoRef)
return (
<Box>
<Card>
<Flex direction="column" align="stretch" gap="2">
<TextArea
value={todo.content}
onChange={e => setTodo(
Todo.Todo.make({ ...todo, content: e.target.value }, true)
)}
/>
<Flex direction="row" justify="center" align="center">
<Button
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.prepend(todo)),
Effect.flatMap(() => createEmptyTodo),
Effect.map(setTodo),
runSync,
)}
>
Add
</Button>
</Flex>
</Flex>
</Card>
</Box>
)
}

View File

@@ -0,0 +1,56 @@
import { Todo } from "@/domain"
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
import { Effect } from "effect"
import { Delete } from "lucide-react"
import { useState } from "react"
import { Reffuse } from "../reffuse"
import { TodosState } from "../services"
export interface VTodoProps {
readonly index: number
readonly todo: Todo.Todo
}
export function VTodo({ index, todo }: VTodoProps) {
const runSync = Reffuse.useRunSync()
const editorMode = useState(false)
return (
<Box>
<Card>
<Flex direction="column" align="stretch" gap="1">
<TextArea
value={todo.content}
onChange={e => TodosState.TodosState.pipe(
Effect.flatMap(state => state.replace(
index,
Todo.Todo.make({ ...todo, content: e.target.value }, true),
)),
runSync,
)}
disabled={!editorMode}
/>
<Flex direction="row" justify="between" align="center">
<Box></Box>
<Flex direction="row" align="center" gap="1">
<IconButton
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
<Delete />
</IconButton>
</Flex>
</Flex>
</Flex>
</Card>
</Box>
)
}

View File

@@ -0,0 +1,36 @@
import { Box, Flex } from "@radix-ui/themes"
import { Chunk, Effect, Stream } from "effect"
import { Reffuse } from "../reffuse"
import { TodosState } from "../services"
import { VNewTodo } from "./VNewTodo"
import { VTodo } from "./VTodo"
export function VTodos() {
// Sync changes to the todos with the local storage
Reffuse.useFork(TodosState.TodosState.pipe(
Effect.flatMap(state =>
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
)
))
const todosRef = Reffuse.useMemo(TodosState.TodosState.pipe(Effect.map(state => state.todos)))
const [todos] = Reffuse.useRefState(todosRef)
return (
<Flex direction="column" align="center" gap="3">
<Box width="500px">
<VNewTodo />
</Box>
{Chunk.map(todos, (todo, index) => (
<Box key={todo.id} width="500px">
<VTodo index={index} todo={todo} />
</Box>
))}
</Flex>
)
}

View File

@@ -1,5 +1,8 @@
import { Reffuse as RootReffuse } from "@/reffuse" import { GlobalContext } from "@/reffuse"
import { ReffuseContext } from "@thilawyn/reffuse"
import { make } from "@thilawyn/reffuse/Reffuse"
import { PostsState } from "./services" import { PostsState } from "./services"
export const Reffuse = RootReffuse.extend<PostsState.PostsState>() export const PostsContext = ReffuseContext.make<PostsState.PostsState>()
export const Reffuse = make(GlobalContext, PostsContext)

View File

@@ -1,51 +1,27 @@
import { Context, Effect, Fiber, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" import { Context, Effect, ExecutionStrategy, Exit, Fiber, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import React from "react" import React from "react"
import * as ReffuseReactContext from "./ReffuseReactContext.js" import * as ReffuseContext from "./ReffuseContext.js"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class Reffuse< export class Reffuse<R> {
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( constructor(
private readonly runtime: Runtime.Runtime<RuntimeR>, readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
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> { useContext(): Context.Context<R> {
return React.useContext(this.Context).runtime return ReffuseContext.useMergeAll(...this.contexts)
}
useContext(): Context.Context<ContextR> {
return React.useContext(this.Context).context
} }
useRunSync() { useRunSync() {
const { runtime, context } = React.useContext(this.Context) const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback(<A, E>( return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR> effect: Effect.Effect<A, E, R>
): A => effect.pipe( ): A => effect.pipe(
Effect.provide(context), Effect.provide(context),
Runtime.runSync(runtime), Runtime.runSync(runtime),
@@ -53,10 +29,11 @@ export class Reffuse<
} }
useRunPromise() { useRunPromise() {
const { runtime, context } = React.useContext(this.Context) const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback(<A, E>( return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>, effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal }, options?: { readonly signal?: AbortSignal },
): Promise<A> => effect.pipe( ): Promise<A> => effect.pipe(
Effect.provide(context), Effect.provide(context),
@@ -65,10 +42,11 @@ export class Reffuse<
} }
useRunFork() { useRunFork() {
const { runtime, context } = React.useContext(this.Context) const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback(<A, E>( return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>, effect: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions, options?: Runtime.RunForkOptions,
): Fiber.RuntimeFiber<A, E> => effect.pipe( ): Fiber.RuntimeFiber<A, E> => effect.pipe(
Effect.provide(context), Effect.provide(context),
@@ -78,7 +56,7 @@ export class Reffuse<
useMemo<A, E>( useMemo<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>, effect: Effect.Effect<A, E, R>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: RenderOptions, options?: RenderOptions,
): A { ): A {
@@ -90,39 +68,80 @@ export class Reffuse<
]) ])
} }
// useEffect<A, E>( useEffect<A, E>(
// effect: Effect.Effect<A, E, RuntimeR | ContextR | Scope.Scope>, effect: Effect.Effect<A, E, R | Scope.Scope>,
// deps?: React.DependencyList, deps?: React.DependencyList,
// options?: RenderOptions, options?: RenderOptions & ScopeOptions,
// ): void { ): void {
// const runSync = this.useRunSync() const runSync = this.useRunSync()
// return React.useEffect(() => { runSync(effect) }, [ return React.useEffect(() => {
// ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync], const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
// ...(deps ?? []), Effect.tap(scope => Effect.provideService(effect, Scope.Scope, scope)),
// ]) runSync,
// } )
return () => { runSync(Scope.close(scope, Exit.void)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...(deps ?? []),
])
}
useLayoutEffect<A, E>(
effect: Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): void {
const runSync = this.useRunSync()
return React.useLayoutEffect(() => {
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.tap(scope => Effect.provideService(effect, Scope.Scope, scope)),
runSync,
)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...(deps ?? []),
])
}
useSuspense<A, E>( useSuspense<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>, effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal }, deps?: React.DependencyList,
options?: { readonly signal?: AbortSignal } & RenderOptions,
): A { ): A {
const runPromise = this.useRunPromise() const runPromise = this.useRunPromise()
return React.use(runPromise(effect, options))
const promise = React.useMemo(() => runPromise(effect, options), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise],
...(deps ?? []),
])
return React.use(promise)
} }
useFork<A, E>( useFork<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR | Scope.Scope>, effect: Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: Runtime.RunForkOptions & RenderOptions, options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
): void { ): void {
const runSync = this.useRunSync()
const runFork = this.useRunFork() const runFork = this.useRunFork()
return React.useEffect(() => { return React.useEffect(() => {
const fiber = runFork(Effect.scoped(effect), options) const scope = runSync(Scope.make(options?.finalizerExecutionStrategy))
return () => { runFork(Fiber.interrupt(fiber)) } const fiber = runFork(Effect.provideService(effect, Scope.Scope, scope), options)
return () => {
Fiber.interrupt(fiber).pipe(
Effect.flatMap(() => Scope.close(scope, Exit.void)),
runFork,
)
}
}, [ }, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runFork], ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
...(deps ?? []), ...(deps ?? []),
]) ])
} }
@@ -136,7 +155,7 @@ export class Reffuse<
) )
} }
useRefFromEffect<A, E>(effect: Effect.Effect<A, E, RuntimeR | ContextR>): SubscriptionRef.SubscriptionRef<A> { useRefFromEffect<A, E>(effect: Effect.Effect<A, E, R>): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo( return this.useMemo(
effect.pipe(Effect.flatMap(SubscriptionRef.make)), effect.pipe(Effect.flatMap(SubscriptionRef.make)),
[], [],
@@ -173,10 +192,14 @@ export interface RenderOptions {
readonly doNotReExecuteOnRuntimeOrContextChange?: boolean readonly doNotReExecuteOnRuntimeOrContextChange?: boolean
} }
export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
}
export const make = <R = never>(): Reffuse<never, R, R> =>
new Reffuse(Runtime.defaultRuntime)
export const makeWithRuntime = <R = never>() => export const make = <
<RuntimeR>(runtime: Runtime.Runtime<RuntimeR>): Reffuse<RuntimeR, R, R> => const Contexts extends readonly ReffuseContext.ReffuseContext<any>[]
new Reffuse(runtime) >(
...contexts: Contexts
): Reffuse<{ [K in keyof Contexts]: ReffuseContext.R<Contexts[K]> }[number]> =>
new Reffuse(contexts)

View File

@@ -0,0 +1,73 @@
import { Array, Context, Effect, Layer, Runtime } from "effect"
import React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class ReffuseContext<R> {
readonly Context = React.createContext<Context.Context<R>>(null!)
readonly Provider: ReffuseContextReactProvider<R>
constructor() {
this.Provider = (props) => {
const runtime = ReffuseRuntime.useRuntime()
const value = React.useMemo(() => Effect.context<R>().pipe(
Effect.provide(props.layer),
Runtime.runSync(runtime),
), [runtime, props.layer])
return (
<this.Context
{...props}
value={value}
/>
)
}
this.Provider.displayName = "ReffuseContextReactProvider"
}
useContext(): Context.Context<R> {
return React.useContext(this.Context)
}
useLayer(): Layer.Layer<R> {
const context = this.useContext()
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
}
}
export type ReffuseContextReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown>
readonly children?: React.ReactNode
}>
export type R<T> = T extends ReffuseContext<infer R> ? R : never
export function make<R = never>() {
return new ReffuseContext<R>()
}
export function useMergeAll<
const Contexts extends readonly ReffuseContext<any>[]
>(
...contexts: Contexts
): Context.Context<{ [K in keyof Contexts]: R<Contexts[K]> }[number]> {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Context.mergeAll(...values), values)
}
export function useMergeAllLayers<
const Contexts extends Array.NonEmptyReadonlyArray<ReffuseContext<any>>
>(
...contexts: Contexts
): Layer.Layer<{ [K in keyof Contexts]: R<Contexts[K]> }[number]> {
const values = Array.map(contexts, v => React.use(v.Context))
return React.useMemo(() => Layer.mergeAll(
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
) as Layer.Layer<any>, values)
}

View File

@@ -1,84 +0,0 @@
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}
/>
)
}
}

View File

@@ -0,0 +1,15 @@
import { Runtime } from "effect"
import React from "react"
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
export const Provider = (props: { readonly children?: React.ReactNode }) => (
<Context
{...props}
value={Runtime.defaultRuntime}
/>
)
Provider.displayName = "ReffuseRuntimeReactProvider"
export const useRuntime = () => React.useContext(Context)

View File

@@ -1,2 +1,3 @@
export * as Reffuse from "./Reffuse.js" export * as Reffuse from "./Reffuse.js"
export * as ReffuseReactContext from "./ReffuseReactContext.js" export * as ReffuseContext from "./ReffuseContext.js"
export * as ReffuseRuntime from "./ReffuseRuntime.js"

View File

@@ -1,43 +0,0 @@
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"))