From 9d0daaa87f0b7dabcb43f18241d94c551f7c673c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sun, 18 May 2025 10:52:39 +0200 Subject: [PATCH] Todos example refactoring --- packages/example/src/routes/todos.tsx | 4 +- .../example/src/todos/services/TodosState.ts | 43 ++++++++++--------- packages/example/src/todos/views/VNewTodo.tsx | 41 +++++++----------- packages/example/src/todos/views/VTodos.tsx | 9 +--- 4 files changed, 41 insertions(+), 56 deletions(-) diff --git a/packages/example/src/routes/todos.tsx b/packages/example/src/routes/todos.tsx index 17aa88f..89591a8 100644 --- a/packages/example/src/routes/todos.tsx +++ b/packages/example/src/routes/todos.tsx @@ -26,9 +26,9 @@ function Todos() { return ( - + - + ) diff --git a/packages/example/src/todos/services/TodosState.ts b/packages/example/src/todos/services/TodosState.ts index d9b2bea..a3735c8 100644 --- a/packages/example/src/todos/services/TodosState.ts +++ b/packages/example/src/todos/services/TodosState.ts @@ -2,14 +2,14 @@ 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")> - readonly readFromLocalStorage: Effect.Effect - readonly saveToLocalStorage: Effect.Effect + readonly load: Effect.Effect + readonly save: Effect.Effect readonly prepend: (todo: Todo.Todo) => Effect.Effect readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect @@ -20,34 +20,31 @@ export class TodosState extends Context.Tag("TodosState") Layer.effect(TodosState, Effect.gen(function*() { - const todos = yield* SubscriptionRef.make(Chunk.empty()) - 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())), 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) => 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 todos = yield* SubscriptionRef.make(yield* readFromLocalStorage) + + const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v)) + const save = Effect.flatMap(todos, writeToLocalStorage) + 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)) @@ -56,12 +53,16 @@ export const make = (key: string) => Layer.effect(TodosState, Effect.gen(functio // }) - yield* readFromLocalStorage + // Sync changes with local storage + yield* Effect.forkScoped(todos.changes.pipe( + Stream.debounce("500 millis"), + Stream.runForEach(writeToLocalStorage), + )) return { todos, - readFromLocalStorage, - saveToLocalStorage, + load, + save, prepend, replace, remove, diff --git a/packages/example/src/todos/views/VNewTodo.tsx b/packages/example/src/todos/views/VNewTodo.tsx index 4d183b5..c090df6 100644 --- a/packages/example/src/todos/views/VNewTodo.tsx +++ b/packages/example/src/todos/views/VNewTodo.tsx @@ -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 { 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 = Effect.map(Todo.generateUniqueID, id => Todo.Todo.make({ + id, + content: "", + completedAt: Option.none(), +}, true)) export function VNewTodo() { - const runSync = R.useRunSync() + const todoRef = R.useRef(() => createEmptyTodo) + const [content, setContent] = R.useRefState(R.useSubRef(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]) => state.prepend(todo)), + Effect.andThen(createEmptyTodo), + Effect.flatMap(v => Ref.set(todoRef, v)), + ), [todoRef]) return ( @@ -27,23 +29,12 @@ export function VNewTodo() {