import { Array, Cause, Chunk, type Context, Effect, Exit, Fiber, identity, Option, ParseResult, Pipeable, Predicate, Schema, type Scope, Stream } from "effect" import * as Form from "./Form.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 SubmittableFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/SubmittableForm") export type SubmittableFormTypeId = typeof SubmittableFormTypeId export interface SubmittableForm extends Form.Form { readonly [SubmittableFormTypeId]: SubmittableFormTypeId readonly schema: Schema.Schema readonly context: Context.Context readonly mutation: Mutation.Mutation< readonly [value: A, form: SubmittableForm], MA, ME, MR, MP > readonly validationFiber: Subscribable.Subscribable>, never, never> readonly run: Effect.Effect readonly submit: Effect.Effect>, Cause.NoSuchElementException> } export class SubmittableFormImpl extends Pipeable.Class() implements SubmittableForm { readonly [Form.FormTypeId]: Form.FormTypeId = Form.FormTypeId readonly [SubmittableFormTypeId]: SubmittableFormTypeId = SubmittableFormTypeId readonly path = [] as const constructor( readonly schema: Schema.Schema, readonly context: Context.Context, readonly mutation: Mutation.Mutation< readonly [value: A, form: SubmittableForm], MA, ME, MR, MP >, readonly value: Lens.Lens, never, never, never, never>, readonly encodedValue: Lens.Lens, readonly issues: Lens.Lens, readonly validationFiber: Lens.Lens>, never, never, never, never>, readonly isValidating: Subscribable.Subscribable, readonly canCommit: Subscribable.Subscribable, readonly isCommitting: 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.issues, Array.empty()), ), onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), { onSome: e => Effect.flatMap( ParseResult.ArrayFormatter.formatError(e), v => Lens.set(this.issues, v), ), 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.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 => Effect.flatMap( ParseResult.ArrayFormatter.formatError(e), v => Lens.set(this.issues, v), ), onNone: () => Effect.void, }, ) : Effect.void ), this.canCommit.get, ) } } export const isSubmittableForm = (u: unknown): u is SubmittableForm => Predicate.hasProperty(u, SubmittableFormTypeId) export declare namespace makeSubmittable { export interface Options extends Mutation.make.Options< readonly [value: NoInfer, form: SubmittableForm, NoInfer, NoInfer, unknown, unknown, unknown>], MA, ME, MR, MP > { readonly schema: Schema.Schema readonly initialEncodedValue: NoInfer } } export const makeSubmittable = Effect.fnUntraced(function* ( options: makeSubmittable.Options ): Effect.fn.Return< SubmittableForm, MP>, never, Scope.Scope | R | Result.forkEffect.OutputContext > { const mutation = yield* Mutation.make(options) const valueLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none())) const issuesLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Array.empty())) const validationFiberLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none>())) return new SubmittableFormImpl( options.schema, yield* Effect.context(), mutation, valueLens, Lens.fromSubscriptionRef(yield* SubscriptionRef.make(options.initialEncodedValue)), issuesLens, validationFiberLens, Subscribable.map(validationFiberLens, Option.isSome), Subscribable.map( Subscribable.zipLatestAll(valueLens, issuesLens, validationFiberLens, mutation.result), ([value, issues, validationFiber, result]) => ( Option.isSome(value) && Array.isEmptyReadonlyArray(issues) && Option.isNone(validationFiber) && !(Result.isRunning(result) || Result.hasRefreshingFlag(result)) ), ), Subscribable.map(mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)), yield* Effect.makeSemaphore(1), ) }) export declare namespace serviceSubmittable { export interface Options extends makeSubmittable.Options {} } export const serviceSubmittable = ( options: serviceSubmittable.Options ): Effect.Effect< SubmittableForm, MP>, never, Scope.Scope | R | Result.forkEffect.OutputContext > => Effect.tap( makeSubmittable(options), form => Effect.forkScoped(form.run), )