import * as AsyncData from "@typed/async-data" import { Array, Cause, Chunk, Duration, Effect, Equal, Exit, Fiber, flow, identity, Option, ParseResult, pipe, Pipeable, Predicate, Ref, Schema, Scope, Stream, Subscribable, SubscriptionRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as React from "react" import * as Hooks from "./Hooks/index.js" import * as PropertyPath from "./PropertyPath.js" import * as SubscribableInternal from "./Subscribable.js" import * as SubscriptionSubRef from "./SubscriptionSubRef.js" export const FormTypeId: unique symbol = Symbol.for("effect-fc/Form") export type FormTypeId = typeof FormTypeId export interface Form extends Pipeable.Pipeable { readonly [FormTypeId]: FormTypeId readonly schema: Schema.Schema, readonly submit: (value: NoInfer) => Effect.Effect, readonly valueRef: SubscriptionRef.SubscriptionRef>, readonly encodedValueRef: SubscriptionRef.SubscriptionRef, readonly errorRef: SubscriptionRef.SubscriptionRef>, readonly validationFiberRef: SubscriptionRef.SubscriptionRef>> readonly submitStateRef: SubscriptionRef.SubscriptionRef>, readonly canSubmitSubscribable: Subscribable.Subscribable } class FormImpl extends Pipeable.Class() implements Form { readonly [FormTypeId]: FormTypeId = FormTypeId constructor( readonly schema: Schema.Schema, readonly submit: (value: NoInfer) => Effect.Effect, readonly valueRef: SubscriptionRef.SubscriptionRef>, readonly encodedValueRef: SubscriptionRef.SubscriptionRef, readonly errorRef: SubscriptionRef.SubscriptionRef>, readonly validationFiberRef: SubscriptionRef.SubscriptionRef>>, readonly submitStateRef: SubscriptionRef.SubscriptionRef>, readonly canSubmitSubscribable: Subscribable.Subscribable, ) { super() } } export const isForm = (u: unknown): u is Form => Predicate.hasProperty(u, FormTypeId) export namespace make { export interface Options { readonly schema: Schema.Schema readonly initialEncodedValue: NoInfer readonly submit: (value: NoInfer) => Effect.Effect } } export const make: { ( options: make.Options ): Effect.Effect> } = Effect.fnUntraced(function* ( options: make.Options ) { const valueRef = yield* SubscriptionRef.make(Option.none()) const errorRef = yield* SubscriptionRef.make(Option.none()) const validationFiberRef = yield* SubscriptionRef.make(Option.none>()) const submitStateRef = yield* SubscriptionRef.make(AsyncData.noData()) return new FormImpl( options.schema, options.submit, valueRef, yield* SubscriptionRef.make(options.initialEncodedValue), errorRef, validationFiberRef, submitStateRef, pipe( ([value, error, validationFiber, submitState]: readonly [ Option.Option, Option.Option, Option.Option>, AsyncData.AsyncData, ]) => Option.isSome(value) && Option.isNone(error) && Option.isNone(validationFiber) && !AsyncData.isLoading(submitState), filter => SubscribableInternal.make({ get: Effect.map(Effect.all([valueRef, errorRef, validationFiberRef, submitStateRef]), filter), get changes() { return Stream.map( Stream.zipLatestAll( valueRef.changes, errorRef.changes, validationFiberRef.changes, submitStateRef.changes, ), filter, ) }, }), ), ) }) export const run = ( self: Form ): Effect.Effect => Stream.runForEach( self.encodedValueRef.changes, encodedValue => self.validationFiberRef.pipe( Effect.andThen(Option.match({ onSome: Fiber.interrupt, onNone: () => Effect.void, })), Effect.andThen( Effect.addFinalizer(() => SubscriptionRef.set(self.validationFiberRef, Option.none())).pipe( Effect.andThen(Schema.decode(self.schema, { errors: "all" })(encodedValue)), Effect.exit, Effect.andThen(flow( Exit.matchEffect({ onSuccess: v => Effect.andThen( SubscriptionRef.set(self.valueRef, Option.some(v)), SubscriptionRef.set(self.errorRef, Option.none()), ), onFailure: c => Option.match( Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), { onSome: e => SubscriptionRef.set(self.errorRef, Option.some(e)), onNone: () => Effect.void, }, ), }), Effect.uninterruptible, )), Effect.scoped, Effect.forkScoped, ) ), Effect.andThen(fiber => SubscriptionRef.set(self.validationFiberRef, Option.some(fiber))) ), ) export const submit = ( self: Form ): Effect.Effect>, NoSuchElementException, SR> => Effect.whenEffect( self.valueRef.pipe( Effect.andThen(identity), Effect.tap(Ref.set(self.submitStateRef, AsyncData.loading())), Effect.andThen(flow( self.submit, Effect.exit, Effect.map(Exit.match({ onSuccess: a => AsyncData.success(a), onFailure: e => AsyncData.failure(e), })), Effect.tap(v => Ref.set(self.submitStateRef, v)) )), ), self.canSubmitSubscribable.get, ) export namespace service { export interface Options extends make.Options {} } export const service = ( options: service.Options ): Effect.Effect, never, R | Scope.Scope> => Effect.tap( make(options), form => Effect.forkScoped(run(form)), ) export const field = >>( self: Form, path: P, ): FormField, PropertyPath.ValueFromPath> => new FormFieldImpl( pipe( Option.match({ onSome: (v: A) => Option.map(PropertyPath.get(v, path), Option.some), onNone: () => Option.some(Option.none()), }), filter => SubscribableInternal.make({ get: Effect.flatMap(self.valueRef, filter), get changes() { return Stream.flatMap(self.valueRef.changes, filter) }, }), ), SubscriptionSubRef.makeFromPath(self.encodedValueRef, path), pipe( Option.match({ onSome: (v: ParseResult.ParseError) => Effect.andThen( ParseResult.ArrayFormatter.formatError(v), Array.filter(issue => PropertyPath.equivalence(issue.path, path)), ), onNone: () => Effect.succeed([]), }), filter => SubscribableInternal.make({ get: Effect.flatMap(self.errorRef.get, filter), get changes() { return Stream.flatMap(self.errorRef.changes, filter) }, }), ), pipe( Option.isSome, filter => SubscribableInternal.make({ get: Effect.map(self.validationFiberRef.get, filter), get changes() { return Stream.map(self.validationFiberRef.changes, filter) }, }), ), pipe( AsyncData.isLoading, filter => SubscribableInternal.make({ get: Effect.map(self.submitStateRef, filter), get changes() { return Stream.map(self.submitStateRef.changes, filter) }, }), ), ) export const FormFieldTypeId: unique symbol = Symbol.for("effect-fc/FormField") export type FormFieldTypeId = typeof FormFieldTypeId export interface FormField extends Pipeable.Pipeable { readonly [FormFieldTypeId]: FormFieldTypeId readonly valueSubscribable: Subscribable.Subscribable, NoSuchElementException> readonly encodedValueRef: SubscriptionRef.SubscriptionRef readonly issuesSubscribable: Subscribable.Subscribable readonly isValidatingSubscribable: Subscribable.Subscribable readonly isSubmittingSubscribable: Subscribable.Subscribable } class FormFieldImpl extends Pipeable.Class() implements FormField { readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId constructor( readonly valueSubscribable: Subscribable.Subscribable, NoSuchElementException>, readonly encodedValueRef: SubscriptionRef.SubscriptionRef, readonly issuesSubscribable: Subscribable.Subscribable, readonly isValidatingSubscribable: Subscribable.Subscribable, readonly isSubmittingSubscribable: Subscribable.Subscribable, ) { super() } } export const isFormField = (u: unknown): u is FormField => Predicate.hasProperty(u, FormFieldTypeId) export namespace useForm { export interface Options extends make.Options {} } export const useForm: { ( options: make.Options, deps: React.DependencyList, ): Effect.Effect, never, R> } = Effect.fnUntraced(function* ( options: make.Options, deps: React.DependencyList, ) { const form = yield* Hooks.useMemo(() => make(options), deps) yield* Hooks.useFork(() => run(form), [form]) return form }) export const useSubmit = ( self: Form ): Effect.Effect< () => Promise>>, never, SR > => Hooks.useCallbackPromise(() => submit(self), [self]) export const useField = >>( self: Form, path: P, ): FormField< PropertyPath.ValueFromPath, PropertyPath.ValueFromPath > => React.useMemo(() => field(self, path), [self, ...path]) export namespace useInput { export interface Options { readonly debounce?: Duration.DurationInput } export interface Result { readonly value: T readonly setValue: React.Dispatch> } } export const useInput: { ( field: FormField, options?: useInput.Options, ): Effect.Effect, NoSuchElementException> } = Effect.fnUntraced(function* ( field: FormField, options?: useInput.Options, ) { const internalValueRef = yield* Hooks.useMemo(() => Effect.andThen(field.encodedValueRef, SubscriptionRef.make), [field]) const [value, setValue] = yield* Hooks.useRefState(internalValueRef) yield* Hooks.useFork(() => Effect.all([ Stream.runForEach( Stream.drop(field.encodedValueRef, 1), upstreamEncodedValue => Effect.whenEffect( Ref.set(internalValueRef, upstreamEncodedValue), Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)), ), ), Stream.runForEach( internalValueRef.changes.pipe( Stream.drop(1), Stream.changesWith(Equal.equivalence()), options?.debounce ? Stream.debounce(options.debounce) : identity, ), internalValue => Ref.set(field.encodedValueRef, internalValue), ), ], { concurrency: "unbounded" }), [field, internalValueRef, options?.debounce]) return { value, setValue } }) export namespace useOptionalInput { export interface Options extends useInput.Options { readonly defaultValue: I } export interface Result extends useInput.Result { readonly enabled: boolean readonly setEnabled: React.Dispatch> } } export const useOptionalInput: { ( field: FormField>, options: useOptionalInput.Options, ): Effect.Effect, NoSuchElementException> } = Effect.fnUntraced(function* ( field: FormField>, options: useOptionalInput.Options, ) { const [enabledRef, internalValueRef] = yield* Hooks.useMemo(() => Effect.andThen( field.encodedValueRef, Option.match({ onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]), onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]), }), ), [field]) const [enabled, setEnabled] = yield* Hooks.useRefState(enabledRef) const [value, setValue] = yield* Hooks.useRefState(internalValueRef) yield* Hooks.useFork(() => Effect.all([ Stream.runForEach( Stream.drop(field.encodedValueRef, 1), upstreamEncodedValue => Effect.whenEffect( Option.match(upstreamEncodedValue, { onSome: v => Effect.andThen( Ref.set(enabledRef, true), Ref.set(internalValueRef, v), ), onNone: () => Effect.andThen( Ref.set(enabledRef, false), Ref.set(internalValueRef, options.defaultValue), ), }), Effect.andThen( Effect.all([enabledRef, internalValueRef]), ([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()), ), ), ), Stream.runForEach( enabledRef.changes.pipe( Stream.zipLatest(internalValueRef.changes), Stream.drop(1), Stream.changesWith(Equal.equivalence()), options?.debounce ? Stream.debounce(options.debounce) : identity, ), ([enabled, internalValue]) => Ref.set(field.encodedValueRef, enabled ? Option.some(internalValue) : Option.none()), ), ], { concurrency: "unbounded" }), [field, enabledRef, internalValueRef, options.debounce]) return { enabled, setEnabled, value, setValue } })