import { type Duration, Effect, flow, identity, Option, type ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import { SetStateAction } from "../../types/index.js"
import { useCallbackSync } from "./useCallbackSync.js"
import { useFork } from "./useFork.js"
import { useOnce } from "./useOnce.js"
import { useSubscribeRefs } from "./useSubscribeRefs.js"
export namespace useOptionalInput {
export interface Options {
readonly schema: Schema.Schema
readonly defaultValue?: A
readonly ref: SubscriptionRef.SubscriptionRef>
readonly debounce?: Duration.DurationInput
}
export interface Result {
readonly value: string
readonly onChange: React.ChangeEventHandler
readonly disabled: boolean
readonly setDisabled: React.Dispatch>
readonly error: Option.Option
}
}
export const useOptionalInput: {
(options: useOptionalInput.Options): Effect.Effect
} = Effect.fnUntraced(function* (options: useOptionalInput.Options) {
const [internalRef, disabledRef] = yield* useOnce(() => Effect.andThen(options.ref, upstreamValue =>
Effect.all([
Effect.andThen(
Option.match(upstreamValue, {
onSome: Schema.encode(options.schema),
onNone: () => options.defaultValue
? Schema.encode(options.schema)(options.defaultValue)
: Effect.succeed(""),
}),
SubscriptionRef.make,
),
SubscriptionRef.make(Option.isNone(upstreamValue)),
])
))
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 encoded value is actually different. This avoids infinite re-render loops.
Stream.runForEach(options.ref.changes, Option.match({
onSome: upstreamValue => Effect.andThen(
Effect.all([Schema.encode(options.schema)(upstreamValue), internalRef]),
([encodedUpstreamValue, internalValue]) => Effect.when(
Effect.andThen(
Ref.set(internalRef, encodedUpstreamValue),
Ref.set(disabledRef, false),
),
() => encodedUpstreamValue !== internalValue,
),
),
onNone: () => Ref.set(disabledRef, true),
})),
// Sync all changes to the internal state with upstream
Stream.runForEach(
internalRef.changes.pipe(options.debounce ? Stream.debounce(options.debounce) : identity),
flow(
Schema.decode(options.schema),
Effect.andThen(v => Ref.set(options.ref, Option.some(v))),
Effect.andThen(() => setError(Option.none())),
Effect.catchTag("ParseError", e => Effect.sync(() => setError(Option.some(e)))),
),
),
], { concurrency: "unbounded" }), [options.ref, options.schema, options.debounce, internalRef])
const [value, disabled] = yield* useSubscribeRefs(internalRef, disabledRef)
const onChange = yield* useCallbackSync((e: React.ChangeEvent) => Ref.set(
internalRef,
e.target.value,
), [internalRef])
const setDisabled = yield* useCallbackSync((setStateAction: React.SetStateAction) =>
Effect.andThen(
Ref.updateAndGet(disabledRef, prevState =>
SetStateAction.value(setStateAction, prevState)
),
disabled => !disabled
? Ref.set(options.ref, Option.none())
: internalRef.pipe(
Effect.andThen(Schema.decode(options.schema)),
Effect.andThen(v => Ref.set(options.ref, Option.some(v))),
),
),
[disabledRef, options.ref, internalRef, options.schema])
return { value, onChange, disabled, setDisabled, error }
})