import { Array, type Cause, Chunk, type Duration, Effect, Equal, Function, identity, Option, Pipeable, Predicate, type Scope, Stream, SubscriptionRef } from "effect" import type * as React from "react" import * as Component from "./Component.js" import * as Lens from "./Lens.js" import * as Subscribable from "./Subscribable.js" export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form") export type FormTypeId = typeof FormTypeId export interface FormIssue { readonly path: readonly PropertyKey[] readonly message: string } export interface Form extends Pipeable.Pipeable { readonly [FormTypeId]: FormTypeId readonly path: P readonly value: Subscribable.Subscribable, ER, never> readonly encodedValue: Lens.Lens readonly issues: Subscribable.Subscribable readonly isValidating: Subscribable.Subscribable readonly canCommit: Subscribable.Subscribable readonly isCommitting: Subscribable.Subscribable } export class FormImpl extends Pipeable.Class implements Form { readonly [FormTypeId]: FormTypeId = FormTypeId constructor( readonly path: P, readonly value: Subscribable.Subscribable, ER, never>, readonly encodedValue: Lens.Lens, readonly issues: Subscribable.Subscribable, readonly isValidating: Subscribable.Subscribable, readonly canCommit: Subscribable.Subscribable, readonly isCommitting: Subscribable.Subscribable, ) { super() } } export const isForm = (u: unknown): u is Form => Predicate.hasProperty(u, FormTypeId) const filterIssuesByPath = ( issues: readonly FormIssue[], path: readonly PropertyKey[], ): readonly FormIssue[] => Array.filter(issues, issue => issue.path.length >= path.length && Array.every(path, (p, i) => p === issue.path[i]) ) export const focusObjectOn: {

( self: Form, key: K, ): Form

( key: K, ): (self: Form) => Form } = Function.dual(2,

( self: Form, key: K, ): Form => { const form = self as unknown as FormImpl const path = [...form.path, key] as const return new FormImpl( path, Subscribable.mapOption(form.value, a => a[key]), Lens.focusObjectOn(form.encodedValue, key), Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)), form.isValidating, form.canCommit, form.isCommitting, ) }) export const focusArrayAt: {

( self: Form, index: number, ): Form

( index: number, ): (self: Form) => Form } = Function.dual(2,

( self: Form, index: number, ): Form => { const form = self as unknown as FormImpl const path = [...form.path, index] as const return new FormImpl( path, Subscribable.mapOptionEffect(form.value, values => Effect.fromOption(Array.get(values, index))), Lens.focusArrayAt(form.encodedValue, index), Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)), form.isValidating, form.canCommit, form.isCommitting, ) }) export const focusTupleAt: {

( self: Form, index: K, ): Form

( index: K, ): (self: Form) => Form } = Function.dual(2,

( self: Form, index: K, ): Form => { const form = self as unknown as FormImpl const path = [...form.path, index] as const return new FormImpl( path, Subscribable.mapOption(form.value, values => values[index]), Lens.focusTupleAt(form.encodedValue, index), Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)), form.isValidating, form.canCommit, form.isCommitting, ) }) export const focusChunkAt: {

( self: Form, Chunk.Chunk, ER, EW>, index: number, ): Form

( index: number, ): (self: Form, Chunk.Chunk, ER, EW>) => Form } = Function.dual(2,

( self: Form, Chunk.Chunk, ER, EW>, index: number, ): Form => { const form = self as unknown as FormImpl, Chunk.Chunk, ER, EW> const path = [...form.path, index] as const return new FormImpl( path, Subscribable.mapOptionEffect(form.value, values => Effect.fromOption(Chunk.get(values, index))), Lens.focusChunkAt(form.encodedValue, index), Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)), form.isValidating, form.canCommit, form.isCommitting, ) }) export namespace useInput { export interface Options { readonly debounce?: Duration.Input } export interface Success { readonly value: T readonly setValue: React.Dispatch> } } export const useInput = Effect.fnUntraced(function*

( form: Form, options?: useInput.Options, ): Effect.fn.Return, ER, Scope.Scope> { const internalValueLens = yield* Component.useOnChange(() => Effect.gen(function*() { const internalValueLens = yield* Lens.get(form.encodedValue).pipe( Effect.flatMap(SubscriptionRef.make), Effect.map(Lens.fromSubscriptionRef), ) yield* Effect.forkScoped(Effect.all([ Stream.runForEach( Stream.drop(form.encodedValue.changes, 1), upstreamEncodedValue => Effect.flatMap( Lens.get(internalValueLens), internalValue => !Equal.equals(upstreamEncodedValue, internalValue) ? Lens.set(internalValueLens, upstreamEncodedValue) : Effect.succeed(undefined), ), ), Stream.runForEach( internalValueLens.changes.pipe( Stream.drop(1), Stream.changesWith(Equal.asEquivalence()), options?.debounce ? Stream.debounce(options.debounce) : identity, ), internalValue => Lens.set(form.encodedValue, internalValue), ), ], { concurrency: "unbounded", discard: true })) return internalValueLens }), [form, options?.debounce]) const [value, setValue] = yield* Lens.useState(internalValueLens) return { value, setValue } }) export namespace useOptionalInput { export interface Options extends useInput.Options { readonly defaultValue: T } export interface Success extends useInput.Success { readonly enabled: boolean readonly setEnabled: React.Dispatch> } } export const useOptionalInput = Effect.fnUntraced(function*

( field: Form, ER, EW>, options: useOptionalInput.Options, ): Effect.fn.Return, ER, Scope.Scope> { const [enabledLens, internalValueLens] = yield* Component.useOnChange(() => Effect.gen(function*() { const [enabledLens, internalValueLens] = yield* Effect.flatMap( Lens.get(field.encodedValue), Option.match({ onSome: v => Effect.all([ Effect.map(SubscriptionRef.make(true), Lens.fromSubscriptionRef), Effect.map(SubscriptionRef.make(v), Lens.fromSubscriptionRef), ]), onNone: () => Effect.all([ Effect.map(SubscriptionRef.make(false), Lens.fromSubscriptionRef), Effect.map(SubscriptionRef.make(options.defaultValue), Lens.fromSubscriptionRef), ]), }), ) yield* Effect.forkScoped(Effect.all([ Stream.runForEach( Stream.drop(field.encodedValue.changes, 1), upstreamEncodedValue => Effect.flatMap( Effect.all([Lens.get(enabledLens), Lens.get(internalValueLens)]), ([enabled, internalValue]) => Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()) ? Effect.succeed(undefined) : Option.match(upstreamEncodedValue, { onSome: v => Effect.andThen( Lens.set(enabledLens, true), Lens.set(internalValueLens, v), ), onNone: () => Effect.andThen( Lens.set(enabledLens, false), Lens.set(internalValueLens, options.defaultValue), ), }), ), ), Stream.runForEach( enabledLens.changes.pipe( Stream.zipLatest(internalValueLens.changes), Stream.drop(1), Stream.changesWith(Equal.asEquivalence()), options?.debounce ? Stream.debounce(options.debounce) : identity, ), ([enabled, internalValue]) => Lens.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()), ), ], { concurrency: "unbounded" })) return [enabledLens, internalValueLens] as const }), [field, options.debounce]) const [enabled, setEnabled] = yield* Lens.useState(enabledLens) const [value, setValue] = yield* Lens.useState(internalValueLens) return { enabled, setEnabled, value, setValue } })