0.1.11 (#14)
All checks were successful
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 13s

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/14
This commit was merged in pull request #14.
This commit is contained in:
Julien Valverdé
2025-05-19 14:01:41 +02:00
parent 64943deaab
commit 2c467dc6ec
17 changed files with 217 additions and 215 deletions

View File

@@ -12,12 +12,12 @@
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@tanstack/react-router": "^1.120.2",
"@tanstack/react-router-devtools": "^1.120.2",
"@tanstack/router-plugin": "^1.120.2",
"@tanstack/react-router": "^1.120.3",
"@tanstack/react-router-devtools": "^1.120.3",
"@tanstack/router-plugin": "^1.120.3",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.26.0",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -25,27 +25,27 @@
"globals": "^16.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript-eslint": "^8.32.0",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"
},
"dependencies": {
"@effect/platform": "^0.80.21",
"@effect/platform-browser": "^0.60.12",
"@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.62.1",
"@radix-ui/themes": "^3.2.1",
"@reffuse/extension-lazyref": "workspace:*",
"@reffuse/extension-query": "workspace:*",
"@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.2",
"@typed/lazy-ref": "^0.3.3",
"effect": "^3.14.21",
"lucide-react": "^0.508.0",
"effect": "^3.15.1",
"lucide-react": "^0.510.0",
"mobx": "^6.13.7",
"reffuse": "workspace:*"
},
"overrides": {
"effect": "^3.14.21",
"@effect/platform": "^0.80.21",
"@effect/platform-browser": "^0.60.12",
"effect": "^3.15.1",
"@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.62.1",
"@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1"
}

View File

@@ -1,6 +1,5 @@
import { ThSchema } from "@thilawyn/thilaschema"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect, Schema } from "effect"
import { Schema } from "effect"
export class Todo extends Schema.Class<Todo>("Todo")({
@@ -18,9 +17,4 @@ export const TodoFromJsonStruct = Schema.Struct({
ThSchema.assertEncodedJsonifiable
)
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
export const generateUniqueID = makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom)
)
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)

View File

@@ -9,11 +9,7 @@ export class AppQueryErrorHandler extends QueryErrorHandler.Service<AppQueryErro
"AppQueryErrorHandler",
(self, failure, defect) => self.pipe(
Effect.catchTags({
RequestError: failure,
ResponseError: failure,
}),
Effect.catchTag("RequestError", "ResponseError", failure),
Effect.catchAllDefect(defect),
),
) {}

View File

@@ -2,7 +2,7 @@ import { R } from "@/reffuse"
import { Button, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Console, Effect, Option, Scope } from "effect"
import { Console, Effect, Option } from "effect"
import { useEffect, useState } from "react"
@@ -24,13 +24,13 @@ function RouteComponent() {
const uuidStream = R.useStreamFromReactiveValues([uuid])
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
const scope = R.useScope([uuid])
const [, scopeLayer] = R.useScope([uuid])
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
Effect.andThen(Console.log("Scope changed")),
Effect.provideService(Scope.Scope, scope),
Effect.provide(scopeLayer),
runSync,
), [scope, runSync])
), [scopeLayer, runSync])
return (
<Flex direction="column" justify="center" align="center" gap="2">

View File

@@ -26,7 +26,7 @@ function Todos() {
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
<VTodos />
</TodosContext.Provider>
</Container>

View File

@@ -2,68 +2,43 @@ 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"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, 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>
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
}>() {}
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(Schema.decode(
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
)),
Effect.flatMap(v => Ref.set(todos, v)),
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
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,
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => values.pipe(
Schema.encode(
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
),
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 todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
const save = Effect.flatMap(todos, writeToLocalStorage)
// const moveUp = (index: number) => Effect.gen(function*() {
// Sync changes with local storage
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
// })
yield* readFromLocalStorage
return {
todos,
readFromLocalStorage,
saveToLocalStorage,
prepend,
replace,
remove,
}
return { todos, load, save }
}))

View File

@@ -1,25 +1,27 @@
import { Todo } from "@/domain"
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
import { Effect, Option, SubscriptionRef } from "effect"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, Effect, Option, Ref } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
const createEmptyTodo = Todo.generateUniqueID.pipe(
Effect.map(id => Todo.Todo.make({
id,
content: "",
completedAt: Option.none(),
}, true))
const createEmptyTodo = makeUuid4.pipe(
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
Effect.provide(GetRandomValues.CryptoRandom),
)
export function VNewTodo() {
const runSync = R.useRunSync()
const todoRef = R.useRef(() => createEmptyTodo)
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
const todoRef = R.useMemo(() => createEmptyTodo.pipe(Effect.flatMap(SubscriptionRef.make)), [])
const [todo, setTodo] = R.useRefState(todoRef)
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
Effect.andThen(createEmptyTodo),
Effect.flatMap(v => Ref.set(todoRef, v)),
), [todoRef])
return (
@@ -27,23 +29,12 @@ export function VNewTodo() {
<Card>
<Flex direction="column" align="stretch" gap="2">
<TextArea
value={todo.content}
onChange={e => setTodo(prev =>
Todo.Todo.make({ ...prev, content: e.target.value }, true)
)}
value={content}
onChange={e => setContent(e.target.value)}
/>
<Flex direction="row" justify="center" align="center">
<Button
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.prepend(todo)),
Effect.andThen(createEmptyTodo),
Effect.map(setTodo),
runSync,
)}
>
Add
</Button>
<Button onClick={add}>Add</Button>
</Flex>
</Flex>
</Card>

View File

@@ -1,20 +1,28 @@
import { Todo } from "@/domain"
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
import { Effect } from "effect"
import { Effect, Ref, Stream, SubscriptionRef } from "effect"
import { Delete } from "lucide-react"
import { useState } from "react"
import { R } from "../reffuse"
import { TodosState } from "../services"
export interface VTodoProps {
readonly index: number
readonly todo: Todo.Todo
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
readonly remove: Effect.Effect<void>
}
export function VTodo({ index, todo }: VTodoProps) {
export function VTodo({ todoRef, remove }: VTodoProps) {
const runSync = R.useRunSync()
const localTodoRef = R.useRef(() => todoRef)
const [content, setContent] = R.useRefState(R.useSubRefFromPath(localTodoRef, ["content"]))
R.useFork(() => localTodoRef.changes.pipe(
Stream.debounce("250 millis"),
Stream.runForEach(v => Ref.set(todoRef, v)),
), [localTodoRef])
const editorMode = useState(false)
@@ -23,14 +31,8 @@ export function VTodo({ index, todo }: VTodoProps) {
<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,
)}
value={content}
onChange={e => setContent(e.target.value)}
disabled={!editorMode}
/>
@@ -38,12 +40,7 @@ export function VTodo({ index, todo }: VTodoProps) {
<Box></Box>
<Flex direction="row" align="center" gap="1">
<IconButton
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
<IconButton onClick={() => runSync(remove)}>
<Delete />
</IconButton>
</Flex>

View File

@@ -1,5 +1,5 @@
import { Box, Flex } from "@radix-ui/themes"
import { Chunk, Effect, Stream } from "effect"
import { Chunk, Effect, Ref } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
import { VNewTodo } from "./VNewTodo"
@@ -8,14 +8,7 @@ import { VTodo } from "./VTodo"
export function VTodos() {
// Sync changes to the todos with the local storage
R.useFork(() => TodosState.TodosState.pipe(
Effect.flatMap(state =>
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
)
), [])
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
const [todos] = R.useSubscribeRefs(todosRef)
@@ -27,7 +20,16 @@ export function VTodos() {
{Chunk.map(todos, (todo, index) => (
<Box key={todo.id} width="500px">
<VTodo index={index} todo={todo} />
<R.SubRefFromGetSet
parent={todosRef}
getter={parentValue => Chunk.unsafeGet(parentValue, index)}
setter={(parentValue, value) => Chunk.replace(parentValue, index, value)}
>
{ref => <VTodo
todoRef={ref}
remove={Ref.update(todosRef, Chunk.remove(index))}
/>}
</R.SubRefFromGetSet>
</Box>
))}
</Flex>