import { Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, identity, Option, type ParseResult, Pipeable, Predicate, Schema, type Scope, Stream } from "effect" import type * as React from "react" import * as Component from "./Component.js" import * as Lens from "./Lens.js" import * as Mutation from "./Mutation.js" import * as Result from "./Result.js" import * as Subscribable from "./Subscribable.js" import * as SubscriptionRef from "./SubscriptionRef.js" export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form") export type FormTypeId = typeof FormTypeId export interface Form extends Pipeable.Pipeable { readonly [FormTypeId]: FormTypeId readonly path: P readonly value: Subscribable.Subscribable, ER, never> readonly encodedValue: Lens.Lens readonly error: Subscribable.Subscribable, ER, never> readonly isValidating: Subscribable.Subscribable readonly canSubmit: 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 error: Subscribable.Subscribable, ER, never>, readonly isValidating: Subscribable.Subscribable, readonly canSubmit: Subscribable.Subscribable, ) { super() } } export const RootFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/RootForm") export type RootFormTypeId = typeof RootFormTypeId export interface RootForm extends Form { readonly schema: Schema.Schema readonly context: Context.Context readonly mutation: Mutation.Mutation< readonly [value: A, form: RootForm], MA, ME, MR, MP > readonly autosubmit: boolean readonly validationFiber: Subscribable.Subscribable>, never, never> readonly run: Effect.Effect readonly submit: Effect.Effect>, Cause.NoSuchElementException> } export class RootFormImpl extends Pipeable.Class() implements RootForm { readonly [FormTypeId]: FormTypeId = FormTypeId readonly [RootFormTypeId]: RootFormTypeId = RootFormTypeId readonly path = [] as const constructor( readonly schema: Schema.Schema, readonly context: Context.Context, readonly mutation: Mutation.Mutation< readonly [value: A, form: RootForm], MA, ME, MR, MP >, readonly autosubmit: boolean, readonly value: Lens.Lens, never, never, never, never>, readonly encodedValue: Lens.Lens, readonly error: Lens.Lens, never, never, never, never>, readonly validationFiber: Lens.Lens>, never, never, never, never>, readonly isValidating: Subscribable.Subscribable, readonly canSubmit: Subscribable.Subscribable, readonly runSemaphore: Effect.Semaphore, ) { super() } get run(): Effect.Effect { return this.runSemaphore.withPermits(1)(Stream.runForEach( this.encodedValue.changes, encodedValue => Lens.get(this.validationFiber).pipe( Effect.andThen(Option.match({ onSome: Fiber.interrupt, onNone: () => Effect.void, })), Effect.andThen( Effect.forkScoped(Effect.onExit( Schema.decode(this.schema, { errors: "all" })(encodedValue), exit => Effect.andThen( Exit.matchEffect(exit, { onSuccess: v => Effect.andThen( Lens.set(this.value, Option.some(v)), Lens.set(this.error, Option.none()), ), onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), { onSome: e => Lens.set(this.error, Option.some(e)), onNone: () => Effect.void, }), }), Lens.set(this.validationFiber, Option.none()), ), )).pipe( Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))), Effect.andThen(Fiber.join), Effect.andThen(value => this.autosubmit ? Effect.asVoid(Effect.forkScoped(this.submitValue(value))) : Effect.void ), Effect.forkScoped, ) ), Effect.provide(this.context), ), )) } get submit(): Effect.Effect>, Cause.NoSuchElementException> { return Lens.get(this.value).pipe( Effect.andThen(identity), Effect.andThen(value => this.submitValue(value)), ) } submitValue(value: A): Effect.Effect>> { return Effect.whenEffect( Effect.tap( this.mutation.mutate([value, this as any]), result => Result.isFailure(result) ? Option.match( Chunk.findFirst( Cause.failures(result.cause as Cause.Cause), e => e._tag === "ParseError", ), { onSome: e => Lens.set(this.error, Option.some(e)), onNone: () => Effect.void, }, ) : Effect.void ), this.canSubmit.get, ) } } export const isForm = (u: unknown): u is Form => Predicate.hasProperty(u, FormTypeId) export const isRootForm = (u: unknown): u is RootForm => Predicate.hasProperty(u, RootFormTypeId) export declare namespace make { export interface Options extends Mutation.make.Options< readonly [value: NoInfer, form: RootForm, NoInfer, NoInfer, unknown, unknown, unknown>], MA, ME, MR, MP > { readonly schema: Schema.Schema readonly initialEncodedValue: NoInfer readonly autosubmit?: boolean readonly debounce?: Duration.DurationInput } } export const make = Effect.fnUntraced(function* ( options: make.Options ): Effect.fn.Return< RootForm, MP>, never, Scope.Scope | R | Result.forkEffect.OutputContext > { const mutation = yield* Mutation.make(options) const valueLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none())) const errorLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none())) const validationFiberLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none>())) return new RootFormImpl( options.schema, yield* Effect.context(), mutation, options.autosubmit ?? false, valueLens, Lens.fromSubscriptionRef(yield* SubscriptionRef.make(options.initialEncodedValue)), errorLens, validationFiberLens, Subscribable.map(validationFiberLens, Option.isSome), Subscribable.map( Subscribable.zipLatestAll(valueLens, errorLens, validationFiberLens, mutation.result), ([value, error, validationFiber, result]) => ( Option.isSome(value) && Option.isNone(error) && Option.isNone(validationFiber) && !(Result.isRunning(result) || Result.hasRefreshingFlag(result)) ), ), yield* Effect.makeSemaphore(1), ) }) export declare namespace service { export interface Options extends make.Options {} } export const service = ( options: service.Options ): Effect.Effect< RootForm, MP>, never, Scope.Scope | R | Result.forkEffect.OutputContext > => Effect.tap( make(options), form => Effect.forkScoped(form.run), ) export const focusObjectField =

( self: Form, key: K, ): Form => { const form = self as FormImpl const path = [...form.path, key] as const return new FormImpl( path, Subscribable.map(form.value, Option.map(a => a[key])), Lens.focusObjectField(form.encodedValue, key), form.error, form.isValidating, form.canSubmit, ) } export namespace useInput { export interface Options { readonly debounce?: Duration.DurationInput } 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.whenEffect( Lens.set(internalValueLens, upstreamEncodedValue), Effect.andThen(Lens.get(internalValueLens), internalValue => !Equal.equals(upstreamEncodedValue, internalValue)), ), ), Stream.runForEach( internalValueLens.changes.pipe( Stream.drop(1), Stream.changesWith(Equal.equivalence()), options?.debounce ? Stream.debounce(options.debounce) : identity, ), internalValue => Lens.set(form.encodedValue, internalValue), ), ], { concurrency: "unbounded" })) 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.whenEffect( 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), ), }), Effect.andThen( Effect.all([Lens.get(enabledLens), Lens.get(internalValueLens)]), ([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()), ), ), ), Stream.runForEach( enabledLens.changes.pipe( Stream.zipLatest(internalValueLens.changes), Stream.drop(1), Stream.changesWith(Equal.equivalence()), 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 } })