Compare commits
25 Commits
ec264e0381
...
6526953c37
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6526953c37 | ||
|
|
4d411bc8dc | ||
|
|
0e7f5d93bb | ||
|
|
ca961e9122 | ||
|
|
e340ab0c8e | ||
|
|
2adc6ed186 | ||
|
|
9ff34addcd | ||
|
|
010416f0b1 | ||
|
|
14e028e8c8 | ||
|
|
53ed31be8d | ||
|
|
b46ba311ae | ||
|
|
8e6adc6b85 | ||
|
|
3813e63982 | ||
|
|
f3f44d9abe | ||
|
|
9acf34ee4a | ||
|
|
4a525d5f5d | ||
|
|
8b5c6169da | ||
|
|
e142010128 | ||
|
|
3cc34a2ed1 | ||
|
|
15c1fdd54c | ||
|
|
cb798ad466 | ||
|
|
75c3ad31d0 | ||
|
|
102e8a12b6 | ||
|
|
52b7b071f4 | ||
|
|
9f08894b61 |
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[install.scopes]
|
||||||
|
"@thilawyn" = "https://git.valverde.cloud/api/packages/thilawyn/npm/"
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
packages/example/src/domain/Todo.ts
Normal file
26
packages/example/src/domain/Todo.ts
Normal 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)
|
||||||
|
)
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * as Post from "./Post"
|
export * as Post from "./Post"
|
||||||
|
export * as Todo from "./Todo"
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
packages/example/src/todos/reffuse.ts
Normal file
8
packages/example/src/todos/reffuse.ts
Normal 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)
|
||||||
69
packages/example/src/todos/services/TodosState.ts
Normal file
69
packages/example/src/todos/services/TodosState.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}))
|
||||||
1
packages/example/src/todos/services/index.ts
Normal file
1
packages/example/src/todos/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as TodosState from "./TodosState"
|
||||||
52
packages/example/src/todos/views/VNewTodo.tsx
Normal file
52
packages/example/src/todos/views/VNewTodo.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
56
packages/example/src/todos/views/VTodo.tsx
Normal file
56
packages/example/src/todos/views/VTodo.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
36
packages/example/src/todos/views/VTodos.tsx
Normal file
36
packages/example/src/todos/views/VTodos.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
73
packages/reffuse/src/ReffuseContext.tsx
Normal file
73
packages/reffuse/src/ReffuseContext.tsx
Normal 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)
|
||||||
|
}
|
||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
packages/reffuse/src/ReffuseRuntime.tsx
Normal file
15
packages/reffuse/src/ReffuseRuntime.tsx
Normal 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)
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"))
|
|
||||||
Reference in New Issue
Block a user