import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect" import type * as React from "react" import * as Component from "./Component.js" import * as Mutation from "./Mutation.js" import * as PropertyPath from "./PropertyPath.js" import * as Result from "./Result.js" import * as Subscribable from "./Subscribable.js" import * as SubscriptionRef from "./SubscriptionRef.js" import * as SubscriptionSubRef from "./SubscriptionSubRef.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 schema: Schema.Schema readonly context: Context.Context readonly mutation: Mutation.Mutation< readonly [value: A, form: Form], MA, ME, MR, MP > readonly autosubmit: boolean readonly debounce: Option.Option readonly value: Subscribable.Subscribable> readonly encodedValue: SubscriptionRef.SubscriptionRef readonly error: Subscribable.Subscribable> readonly validationFiber: Subscribable.Subscribable>> readonly canSubmit: Subscribable.Subscribable field>( path: P ): Effect.Effect, PropertyPath.ValueFromPath>> readonly run: Effect.Effect readonly submit: Effect.Effect>, Cause.NoSuchElementException> } export class FormImpl extends Pipeable.Class() implements Form { readonly [FormTypeId]: FormTypeId = FormTypeId constructor( readonly schema: Schema.Schema, readonly context: Context.Context, readonly mutation: Mutation.Mutation< readonly [value: A, form: Form], MA, ME, MR, MP >, readonly autosubmit: boolean, readonly debounce: Option.Option, readonly value: SubscriptionRef.SubscriptionRef>, readonly encodedValue: SubscriptionRef.SubscriptionRef, readonly error: SubscriptionRef.SubscriptionRef>, readonly validationFiber: SubscriptionRef.SubscriptionRef>>, readonly runSemaphore: Effect.Semaphore, readonly fieldCache: Ref.Ref>>, ) { super() this.canSubmit = Subscribable.map( Subscribable.zipLatestAll(this.value, this.error, this.validationFiber, this.mutation.result), ([value, error, validationFiber, result]) => ( Option.isSome(value) && Option.isNone(error) && Option.isNone(validationFiber) && !(Result.isRunning(result) || Result.hasRefreshingFlag(result)) ), ) } field>( path: P ): Effect.Effect, PropertyPath.ValueFromPath>> { const key = new FormFieldKey(path) return this.fieldCache.pipe( Effect.map(HashMap.get(key)), Effect.flatMap(Option.match({ onSome: v => Effect.succeed(v as FormField, PropertyPath.ValueFromPath>), onNone: () => Effect.tap( Effect.succeed(makeFormField(this as Form, path)), v => Ref.update(this.fieldCache, HashMap.set(key, v as FormField)), ), })), ) } readonly canSubmit: Subscribable.Subscribable get run(): Effect.Effect { return this.runSemaphore.withPermits(1)(Stream.runForEach( this.encodedValue.changes.pipe( Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity ), encodedValue => 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( Ref.set(this.value, Option.some(v)), Ref.set(this.error, Option.none()), ), onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), { onSome: e => Ref.set(this.error, Option.some(e)), onNone: () => Effect.void, }), }), Ref.set(this.validationFiber, Option.none()), ), )).pipe( Effect.tap(fiber => Ref.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 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 => Ref.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 declare namespace make { export interface Options extends Mutation.make.Options< readonly [value: NoInfer, form: Form, 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< Form, MP>, never, Scope.Scope | R | Result.forkEffect.OutputContext > { return new FormImpl( options.schema, yield* Effect.context(), yield* Mutation.make(options), options.autosubmit ?? false, Option.fromNullable(options.debounce), yield* SubscriptionRef.make(Option.none()), yield* SubscriptionRef.make(options.initialEncodedValue), yield* SubscriptionRef.make(Option.none()), yield* SubscriptionRef.make(Option.none>()), yield* Effect.makeSemaphore(1), yield* Ref.make(HashMap.empty>()), ) }) export declare namespace service { export interface Options extends make.Options {} } export const service = ( options: service.Options ): Effect.Effect< Form, MP>, never, Scope.Scope | R | Result.forkEffect.OutputContext > => Effect.tap( make(options), form => Effect.forkScoped(form.run), ) export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField") export type FormFieldTypeId = typeof FormFieldTypeId export interface FormField extends Pipeable.Pipeable { readonly [FormFieldTypeId]: FormFieldTypeId readonly value: Subscribable.Subscribable, Cause.NoSuchElementException> readonly encodedValue: SubscriptionRef.SubscriptionRef readonly issues: Subscribable.Subscribable readonly isValidating: Subscribable.Subscribable readonly isSubmitting: Subscribable.Subscribable } class FormFieldImpl extends Pipeable.Class() implements FormField { readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId constructor( readonly value: Subscribable.Subscribable, Cause.NoSuchElementException>, readonly encodedValue: SubscriptionRef.SubscriptionRef, readonly issues: Subscribable.Subscribable, readonly isValidating: Subscribable.Subscribable, readonly isSubmitting: Subscribable.Subscribable, ) { super() } } const FormFieldKeyTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormFieldKey") type FormFieldKeyTypeId = typeof FormFieldKeyTypeId class FormFieldKey implements Equal.Equal { readonly [FormFieldKeyTypeId]: FormFieldKeyTypeId = FormFieldKeyTypeId constructor(readonly path: PropertyPath.PropertyPath) {} [Equal.symbol](that: Equal.Equal) { return isFormFieldKey(that) && PropertyPath.equivalence(this.path, that.path) } [Hash.symbol]() { return Hash.array(this.path) } } export const isFormField = (u: unknown): u is FormField => Predicate.hasProperty(u, FormFieldTypeId) const isFormFieldKey = (u: unknown): u is FormFieldKey => Predicate.hasProperty(u, FormFieldKeyTypeId) export const makeFormField = >>( self: Form, path: P, ): FormField, PropertyPath.ValueFromPath> => { return new FormFieldImpl( Subscribable.mapEffect(self.value, Option.match({ onSome: v => Option.map(PropertyPath.get(v, path), Option.some), onNone: () => Option.some(Option.none()), })), SubscriptionSubRef.makeFromPath(self.encodedValue, path), Subscribable.mapEffect(self.error, Option.match({ onSome: flow( ParseResult.ArrayFormatter.formatError, Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))), ), onNone: () => Effect.succeed([]), })), Subscribable.map(self.validationFiber, Option.isSome), Subscribable.map(self.mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)), ) } 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* ( field: FormField, options?: useInput.Options, ): Effect.fn.Return, Cause.NoSuchElementException, Scope.Scope> { const internalValueRef = yield* Component.useOnChange(() => Effect.tap( Effect.andThen(field.encodedValue, SubscriptionRef.make), internalValueRef => Effect.forkScoped(Effect.all([ Stream.runForEach( Stream.drop(field.encodedValue, 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.encodedValue, internalValue), ), ], { concurrency: "unbounded" })), ), [field, options?.debounce]) const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef) 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: FormField>, options: useOptionalInput.Options, ): Effect.fn.Return, Cause.NoSuchElementException, Scope.Scope> { const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap( Effect.andThen( field.encodedValue, Option.match({ onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]), onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]), }), ), ([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([ Stream.runForEach( Stream.drop(field.encodedValue, 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.encodedValue, enabled ? Option.some(internalValue) : Option.none()), ), ], { concurrency: "unbounded" })), ), [field, options.debounce]) const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef) const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef) return { enabled, setEnabled, value, setValue } })