import { Array, Cause, type Context, Effect, Fiber, Option, Pipeable, Predicate, Schema, SchemaIssue, SchemaParser, type Scope, Semaphore, SubscriptionRef, } 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" type FormSchema = Schema.Top & { readonly Type: A readonly Encoded: I readonly DecodingServices: R } 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: FormSchema 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.NoSuchElementError> } export class SubmittableFormImpl extends Pipeable.Class implements SubmittableForm { readonly [Form.FormTypeId]: Form.FormTypeId = Form.FormTypeId readonly [SubmittableFormTypeId]: SubmittableFormTypeId = SubmittableFormTypeId readonly path = [] as const readonly encodedValue: Lens.Lens readonly isValidating: Subscribable.Subscribable readonly canCommit: Subscribable.Subscribable readonly isCommitting: Subscribable.Subscribable constructor( readonly schema: FormSchema, 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 internalEncodedValue: Lens.Lens, readonly issues: Lens.Lens, readonly validationFiber: Lens.Lens>, never, never, never, never>, readonly runSemaphore: Semaphore.Semaphore, ) { super() this.encodedValue = Lens.make({ get: Lens.get(internalEncodedValue), changes: internalEncodedValue.changes, commit: encoded => Effect.andThen( Lens.set(internalEncodedValue, encoded), this.synchronizeEncodedValue(encoded), ), lock: Lens.asLensImpl(internalEncodedValue).lock, }) this.isValidating = Subscribable.map(validationFiber, Option.isSome) const commitState = Subscribable.zipLatestAll( value as any, issues as any, validationFiber as any, mutation.result as any, ) as unknown as Subscribable.Subscribable, readonly Form.FormIssue[], Option.Option>, Result.Result, ]> this.canCommit = Subscribable.map( commitState, ([current, currentIssues, fiber, result]: readonly [Option.Option, readonly Form.FormIssue[], Option.Option>, Result.Result]) => Option.isSome(current) && currentIssues.length === 0 && Option.isNone(fiber) && !(Result.isRunning(result) || Result.hasRefreshingFlag(result)), ) this.isCommitting = Subscribable.map( mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result), ) } synchronizeEncodedValue(encodedValue: I): Effect.Effect { const self = this return Effect.gen(function*() { const current = yield* Lens.get(self.validationFiber) if (Option.isSome(current)) yield* Fiber.interrupt(current.value) const fiber = yield* Effect.forkScoped( Effect.ensuring( SchemaParser.decodeEffect(self.schema)(encodedValue), Lens.set(self.validationFiber, Option.none()), ), ) yield* Lens.set(self.validationFiber, Option.some(fiber)) const decoded = yield* Fiber.join(fiber).pipe( Effect.tap(value => Effect.andThen( Lens.set(self.issues, Array.empty()), Lens.set(self.value, Option.some(value)), )), Effect.catchIf(SchemaIssue.isIssue, issue => Lens.set(self.issues, formatIssue(issue))), ) void decoded }).pipe(Effect.provide(this.context)) as Effect.Effect } get run(): Effect.Effect { return Effect.flatMap( Lens.get(this.encodedValue), SchemaParser.decodeEffect(this.schema), ).pipe( Effect.option, Effect.flatMap(value => Lens.set(this.value, value)), Effect.provide(this.context), this.runSemaphore.withPermits(1), ) } get submit(): Effect.Effect>, Cause.NoSuchElementError> { return Effect.flatMap(Lens.get(this.value), value => Effect.flatMap(Effect.fromOption(value), decoded => this.submitValue(decoded))) } submitValue(value: A): Effect.Effect>> { return Effect.flatMap(this.canCommit.get, canCommit => { if (!canCommit) return Effect.succeed(Option.none()) return Effect.map( Effect.tap(this.mutation.mutate([value, this as any]), result => { if (!Result.isFailure(result)) return Effect.succeed(undefined) const issue = Cause.findErrorOption(result.cause) return Option.isSome(issue) && SchemaIssue.isIssue(issue.value) ? Lens.set(this.issues, formatIssue(issue.value)) : Effect.succeed(undefined) }), Option.some, ) }) } } const formatIssue = (issue: SchemaIssue.Issue): readonly Form.FormIssue[] => { const formatted = SchemaIssue.makeFormatterStandardSchemaV1()(issue) return formatted.issues.map(item => ({ path: (item.path ?? []) as readonly PropertyKey[], message: item.message, })) } export const isSubmittableForm = (u: unknown): u is SubmittableForm => Predicate.hasProperty(u, SubmittableFormTypeId) export declare namespace make { export interface Options extends Mutation.make.Options< readonly [value: NoInfer, form: SubmittableForm, NoInfer, NoInfer, unknown, unknown, unknown>], MA, ME, MR, MP > { readonly schema: FormSchema readonly initialEncodedValue: NoInfer } } export const make = Effect.fnUntraced(function* ( options: make.Options, ): Effect.fn.Return< SubmittableForm, MP>, never, Scope.Scope | R | Result.forkEffect.OutputContext > { return new SubmittableFormImpl( options.schema, yield* Effect.context(), yield* Mutation.make(options), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none())), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(options.initialEncodedValue)), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Array.empty())), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none>())), yield* Semaphore.make(1), ) }) export declare namespace service { export interface Options extends make.Options {} } export const service = ( options: service.Options, ): Effect.Effect< SubmittableForm, MP>, never, Scope.Scope | R | Result.forkEffect.OutputContext > => Effect.tap(make(options), form => Effect.asVoid(Effect.forkScoped(form.run)))