0.1.13 #18

Merged
Thilawyn merged 359 commits from next into master 2025-06-18 00:12:19 +02:00
4 changed files with 41 additions and 56 deletions
Showing only changes of commit 9d0daaa87f - Show all commits

View File

@@ -26,9 +26,9 @@ function Todos() {
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<TodosContext.AsyncProvider layer={todosLayer}>
<VTodos />
</TodosContext.Provider>
</TodosContext.AsyncProvider>
</Container>
)

View File

@@ -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")<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 load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
@@ -20,34 +20,31 @@ export class TodosState extends Context.Tag("TodosState")<TodosState, {
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 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,

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 { Effect, Option, Ref } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
const createEmptyTodo = Todo.generateUniqueID.pipe(
Effect.map(id => Todo.Todo.make({
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() {
<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,5 +1,5 @@
import { Box, Flex } from "@radix-ui/themes"
import { Chunk, Effect, Stream } from "effect"
import { Chunk, Effect } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
import { VNewTodo } from "./VNewTodo"
@@ -8,13 +8,6 @@ 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 [todos] = R.useSubscribeRefs(todosRef)