import { type Duration, Effect, Equal, Equivalence, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import { useFork } from "../useFork.js"
import { useOnce } from "../useOnce.js"
import { useRefState } from "../useRefState.js"
export namespace useInput {
export interface Options {
readonly schema: Schema.Schema
readonly equivalence?: Equivalence.Equivalence
readonly ref: SubscriptionRef.SubscriptionRef
readonly debounce?: Duration.DurationInput
}
export interface Result {
readonly value: string
readonly setValue: React.Dispatch>
readonly error: Option.Option
}
}
export const useInput: {
(options: useInput.Options): Effect.Effect
} = Effect.fnUntraced(function* (options: useInput.Options) {
const internalRef = yield* useOnce(() => options.ref.pipe(
Effect.andThen(Schema.encode(options.schema)),
Effect.andThen(SubscriptionRef.make),
))
const [error, setError] = React.useState(Option.none())
yield* useFork(() => Effect.all([
// Sync the upstream state with the internal state
// Only mutate the internal state if the upstream value is actually different. This avoids infinite re-render loops.
Stream.runForEach(Stream.changesWith(options.ref.changes, Equivalence.strict()), upstreamValue =>
Effect.whenEffect(
Effect.andThen(
Schema.encode(options.schema)(upstreamValue),
encodedUpstreamValue => Ref.set(internalRef, encodedUpstreamValue),
),
internalRef.pipe(
Effect.andThen(Schema.decode(options.schema)),
Effect.andThen(decodedInternalValue => !(options.equivalence ?? Equal.equals)(upstreamValue, decodedInternalValue)),
Effect.catchTag("ParseError", () => Effect.succeed(false)),
),
)
),
// Sync all changes to the internal state with upstream
Stream.runForEach(
internalRef.changes.pipe(
Stream.changesWith(Equivalence.strict()),
options.debounce ? Stream.debounce(options.debounce) : identity,
Stream.drop(1),
),
flow(
Schema.decode(options.schema),
Effect.andThen(v => Ref.set(options.ref, v)),
Effect.andThen(() => setError(Option.none())),
Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))),
),
),
], { concurrency: "unbounded" }), [options.schema, options.equivalence, options.ref, options.debounce, internalRef])
const [value, setValue] = yield* useRefState(internalRef)
return { value, setValue, error }
})