diff --git a/packages/effect-fc/src/hooks/Hooks/useSubscribe.ts b/packages/effect-fc/src/hooks/Hooks/useSubscribe.ts index d78ca2c..29290cd 100644 --- a/packages/effect-fc/src/hooks/Hooks/useSubscribe.ts +++ b/packages/effect-fc/src/hooks/Hooks/useSubscribe.ts @@ -1,18 +1,18 @@ -import { Effect, Equivalence, pipe, type Readable, Stream, type Subscribable } from "effect" +import { Effect, Equivalence, pipe, Stream, type Subscribable } from "effect" import * as React from "react" import { useFork } from "./useFork.js" import { useOnce } from "./useOnce.js" export const useSubscribe: { - & Subscribable.Subscribable)[]>( + []>( ...elements: T ): Effect.Effect< { [K in keyof T]: Effect.Effect.Success | Stream.Stream.Success }, Effect.Effect.Error | Stream.Stream.Error, Effect.Effect.Context | Stream.Stream.Context > -} = Effect.fnUntraced(function* & Subscribable.Subscribable)[]>( +} = Effect.fnUntraced(function* []>( ...elements: T ) { const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => diff --git a/packages/effect-fc/src/types/PropertyPath.ts b/packages/effect-fc/src/types/PropertyPath.ts index dda0959..6b59d37 100644 --- a/packages/effect-fc/src/types/PropertyPath.ts +++ b/packages/effect-fc/src/types/PropertyPath.ts @@ -38,8 +38,7 @@ export type ValueFromPath = P extends [infer Head, ...infer : never : T -export type AnyKey = string | number | symbol -export type AnyPath = readonly AnyKey[] +export type AnyPath = readonly PropertyKey[] export const unsafeGet: { diff --git a/packages/effect-fc/src/types/SubscriptionSubRef.ts b/packages/effect-fc/src/types/SubscriptionSubRef.ts index ead45f1..dfbdc1c 100644 --- a/packages/effect-fc/src/types/SubscriptionSubRef.ts +++ b/packages/effect-fc/src/types/SubscriptionSubRef.ts @@ -1,4 +1,4 @@ -import { Chunk, Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect" +import { Chunk, Effect, Effectable, Option, type Predicate, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect" import * as PropertyPath from "./PropertyPath.js" @@ -7,7 +7,7 @@ export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId export interface SubscriptionSubRef> extends SubscriptionSubRef.Variance, SubscriptionRef.SubscriptionRef { - readonly parent: SubscriptionRef.SubscriptionRef + readonly parent: B readonly [Unify.typeSymbol]?: unknown readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify @@ -105,7 +105,7 @@ export const makeFromPath = < (parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)), ) -export const makeFromChunkRef: { +export const makeFromChunkIndex: { >>( parent: B, index: number, @@ -128,3 +128,30 @@ export const makeFromChunkRef: { parentValue => Chunk.unsafeGet(parentValue, index), (parentValue, value) => Chunk.replace(parentValue, index, value), ) as any + +export const makeFromChunkFindFirst: { + >>( + parent: B, + findFirstPredicate: Predicate.Predicate extends Chunk.NonEmptyChunk ? A : never>, + ): SubscriptionSubRef< + Effect.Effect.Success extends Chunk.NonEmptyChunk ? A : never, + B + > + >>( + parent: B, + findFirstPredicate: Predicate.Predicate extends Chunk.Chunk ? A : never>, + ): SubscriptionSubRef< + Effect.Effect.Success extends Chunk.Chunk ? A : never, + B + > +} = ( + parent: SubscriptionRef.SubscriptionRef>, + findFirstPredicate: Predicate.Predicate.Any, +) => new SubscriptionSubRefImpl( + parent, + parentValue => Option.getOrThrow(Chunk.findFirst(parentValue, findFirstPredicate)), + (parentValue, value) => Option.getOrThrow(Option.andThen( + Chunk.findFirstIndex(parentValue, findFirstPredicate), + index => Chunk.replace(parentValue, index, value), + )), +) as any diff --git a/packages/example/src/todo/Todo.tsx b/packages/example/src/todo/Todo.tsx index c099613..3aca33b 100644 --- a/packages/example/src/todo/Todo.tsx +++ b/packages/example/src/todo/Todo.tsx @@ -4,7 +4,7 @@ import { TextAreaInput } from "@/lib/TextAreaInput" import { TextFieldInput } from "@/lib/TextFieldInput" import { Box, Button, Flex, IconButton } from "@radix-ui/themes" import { GetRandomValues, makeUuid4 } from "@typed/id" -import { Chunk, DateTime, Effect, Match, Option, Ref, Runtime, Schema, SubscriptionRef } from "effect" +import { Chunk, DateTime, Effect, flow, identity, Match, Option, Ref, Runtime, Schema, SubscriptionRef } from "effect" import { Component, Memo } from "effect-fc" import { useMemo, useOnce, useSubscribe } from "effect-fc/hooks" import { SubscriptionSubRef } from "effect-fc/types" @@ -14,7 +14,7 @@ import { TodosState } from "./TodosState.service" const StringTextAreaInput = TextAreaInput({ schema: Schema.String }) -const OptionalDateInput = TextFieldInput({ optional: true, schema: DateTimeUtcFromZonedInput }) +const OptionalDateTimeInput = TextFieldInput({ optional: true, schema: DateTimeUtcFromZonedInput }) const makeTodo = makeUuid4.pipe( Effect.map(id => Domain.Todo.Todo.make({ @@ -27,31 +27,38 @@ const makeTodo = makeUuid4.pipe( export type TodoProps = ( - | { readonly _tag: "new", readonly index?: never } - | { readonly _tag: "edit", readonly index: number } + | { readonly _tag: "new" } + | { readonly _tag: "edit", readonly id: string } ) export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps) { const runtime = yield* Effect.runtime() const state = yield* TodosState - const [ref, contentRef, completedAtRef] = yield* useMemo(() => Match.value(props).pipe( - Match.tag("new", () => Effect.andThen(makeTodo, SubscriptionRef.make)), - Match.tag("edit", ({ index }) => Effect.succeed(SubscriptionSubRef.makeFromChunkRef(state.ref, index))), + const { ref, indexRef, contentRef, completedAtRef } = yield* useMemo(() => Match.value(props).pipe( + Match.tag("new", () => Effect.Do.pipe( + Effect.bind("ref", () => Effect.andThen(makeTodo, SubscriptionRef.make)), + Effect.bind("indexRef", () => SubscriptionRef.make(-1)), + )), + Match.tag("edit", ({ id }) => Effect.Do.pipe( + Effect.let("ref", () => SubscriptionSubRef.makeFromChunkFindFirst(state.ref, v => v.id === id)), + Effect.let("indexRef", () => SubscriptionSubRef.makeFromGetSet(state.ref, { + get: flow(Chunk.findFirstIndex(v => v.id === id), Option.getOrThrow), + set: identity, + })), + )), Match.exhaustive, - Effect.map(ref => [ - ref, - SubscriptionSubRef.makeFromPath(ref, ["content"]), - SubscriptionSubRef.makeFromPath(ref, ["completedAt"]), - ] as const), + Effect.let("contentRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["content"])), + Effect.let("completedAtRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["completedAt"])), // eslint-disable-next-line react-hooks/exhaustive-deps - ), [props._tag, props.index]) + ), [props._tag, props._tag === "edit" ? props.id : undefined]) - const [size] = yield* useSubscribe(state.sizeRef) + const [index, size] = yield* useSubscribe(indexRef, state.sizeRef) const StringTextAreaInputFC = yield* StringTextAreaInput - const OptionalDateInputFC = yield* OptionalDateInput + const OptionalDateTimeInputFC = yield* OptionalDateTimeInput + return ( @@ -60,7 +67,7 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps - DateTime.now)} @@ -85,30 +92,40 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps {props._tag === "edit" && Runtime.runSync(runtime)( - SubscriptionRef.updateEffect(state.ref, todos => Effect.gen(function*() { - if (props.index <= 0) return yield* Option.none() - return todos.pipe( - Chunk.replace(props.index, yield* Chunk.get(todos, props.index - 1)), - Chunk.replace(props.index - 1, yield* ref), - ) - })) + SubscriptionRef.updateEffect(state.ref, todos => Effect.Do.pipe( + Effect.bind("todo", () => ref), + Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === props.id)), + Effect.bind("previous", () => Chunk.get(todos, index - 1)), + Effect.andThen(({ todo, index, previous }) => index > 0 + ? todos.pipe( + Chunk.replace(index, previous), + Chunk.replace(index - 1, todo), + ) + : todos + ), + )) )} > = size - 1} + disabled={index >= size - 1} onClick={() => Runtime.runSync(runtime)( - SubscriptionRef.updateEffect(state.ref, todos => Effect.gen(function*() { - if (props.index >= size - 1) return yield* Option.none() - return todos.pipe( - Chunk.replace(props.index, yield* Chunk.get(todos, props.index + 1)), - Chunk.replace(props.index + 1, yield* ref), - ) - })) + SubscriptionRef.updateEffect(state.ref, todos => Effect.Do.pipe( + Effect.bind("todo", () => ref), + Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === props.id)), + Effect.bind("next", () => Chunk.get(todos, index + 1)), + Effect.andThen(({ todo, index, next }) => index < Chunk.size(todos) - 1 + ? todos.pipe( + Chunk.replace(index, next), + Chunk.replace(index + 1, todo), + ) + : todos + ), + )) )} > @@ -116,7 +133,10 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps Runtime.runSync(runtime)( - Ref.update(state.ref, Chunk.remove(props.index)) + SubscriptionRef.updateEffect(state.ref, todos => Effect.andThen( + Chunk.findFirstIndex(todos, v => v.id === props.id), + index => Chunk.remove(todos, index), + )) )} > diff --git a/packages/example/src/todo/Todos.tsx b/packages/example/src/todo/Todos.tsx index 593476b..e533f24 100644 --- a/packages/example/src/todo/Todos.tsx +++ b/packages/example/src/todo/Todos.tsx @@ -24,8 +24,8 @@ export class Todos extends Component.makeUntraced(function* Todos() { - {Chunk.map(todos, (v, k) => - + {Chunk.map(todos, todo => + )}