import type { StandardSchemaV1 } from "@standard-schema/spec" import { Array, Cause, type Context, Effect, Fiber, Option, Pipeable, Predicate, Schema, SchemaError, SchemaIssue, type Scope, Semaphore, SubscriptionRef } from "effect" import { AsyncResult } from "effect/unstable/reactivity" import * as Form from "./Form.js" import * as Lens from "./Lens.js" import * as Mutation from "./Mutation.js" import * as View from "./View.js" export const MutationFormTypeId: unique symbol = Symbol.for("@effect-view/Form/MutationForm") export type MutationFormTypeId = typeof MutationFormTypeId export interface MutationForm extends Form.Form { readonly [MutationFormTypeId]: MutationFormTypeId readonly schema: Schema.ConstraintCodec readonly context: Context.Context readonly mutation: Mutation.Mutation< readonly [value: A, form: MutationForm], MA, ME, MR > readonly validationFiber: View.View>, never, never> readonly run: Effect.Effect readonly submit: Effect.Effect | AsyncResult.Failure>, Cause.NoSuchElementError> } export class MutationFormImpl extends Pipeable.Class implements MutationForm { readonly [Form.FormTypeId]: Form.FormTypeId = Form.FormTypeId readonly [MutationFormTypeId]: MutationFormTypeId = MutationFormTypeId readonly path = [] as const readonly encodedValue: Lens.Lens readonly isValidating: View.View readonly canCommit: View.View readonly isCommitting: View.View constructor( readonly schema: Schema.ConstraintCodec, readonly context: Context.Context, readonly mutation: Mutation.Mutation< readonly [value: A, form: MutationForm], MA, ME, MR >, 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 = Effect.all([ Effect.succeed(this), Effect.succeed(Lens.asLensImpl(this.internalEncodedValue)), ]).pipe( Effect.map(([self, parent]) => Lens.make({ get: parent.get, get changes() { return parent.changes }, commit: a => Effect.andThen( Effect.flatMap( parent.resolve, resolved => resolved.commit(Effect.succeed(a)), ), self.synchronizeEncodedValue(a), ), lock: parent.lock, })), Lens.unwrap, ) this.isValidating = Effect.succeed(this).pipe( Effect.map(self => View.map(self.validationFiber, Option.isSome)), View.unwrap, ) this.canCommit = Effect.succeed(this).pipe( Effect.map(self => View.map( View.zipLatestAll(self.value, self.issues, self.validationFiber, self.mutation.state), ([value, issues, validationFiber, result]) => ( Option.isSome(value) && Array.isReadonlyArrayEmpty(issues) && Option.isNone(validationFiber) && !AsyncResult.isWaiting(result) ), )), View.unwrap, ) this.isCommitting = Effect.succeed(this).pipe( Effect.map(self => View.map(self.mutation.state, AsyncResult.isWaiting)), View.unwrap, ) } synchronizeEncodedValue(encodedValue: I): Effect.Effect { return Lens.get(this.validationFiber).pipe( Effect.andThen(Option.match({ onSome: Fiber.interrupt, onNone: () => Effect.void, })), Effect.andThen(Effect.forkScoped( Effect.ensuring( Schema.decodeEffect(this.schema, { errors: "all" })(encodedValue), Lens.set(this.validationFiber, Option.none()), ) )), Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))), Effect.flatMap(Fiber.join), Effect.tap(() => Lens.set(this.issues, Array.empty())), Effect.flatMap(value => Lens.set(this.value, Option.some(value))), Effect.catchIf( SchemaError.isSchemaError, error => Lens.set(this.issues, SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues), ), Effect.provide(this.context), ) } get run(): Effect.Effect { return Lens.get(this.encodedValue).pipe( Effect.flatMap(v => Schema.decodeEffect(this.schema)(v)), Effect.option, Effect.flatMap(v => Lens.set(this.value, v)), Effect.provide(this.context), this.runSemaphore.withPermits(1), ) } get submit(): Effect.Effect | AsyncResult.Failure>, Cause.NoSuchElementError, never> { return Lens.get(this.value).pipe( Effect.flatMap(Effect.fromOption), Effect.flatMap(value => this.submitValue(value)), ) } submitValue(value: A): Effect.Effect | AsyncResult.Failure>, never, never> { return Effect.when( Effect.tap( this.mutation.mutate([value, this as any]), result => AsyncResult.isFailure(result) ? Option.match( Array.findFirst( result.cause.reasons, reason => Cause.isFailReason(reason) && SchemaError.isSchemaError(reason.error) ? Option.some(reason.error) : Option.none(), ), { onSome: error => Lens.set(this.issues, SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues), onNone: () => Effect.void, }, ) : Effect.void, ), View.get(this.canCommit), ) } } export const isMutationForm = (u: unknown): u is MutationForm => Predicate.hasProperty(u, MutationFormTypeId) export declare namespace make { export interface Options extends Mutation.make.Options< readonly [value: NoInfer, form: MutationForm, NoInfer, NoInfer, NoInfer, unknown, unknown, unknown>], MA, ME, MR > { readonly schema: Schema.ConstraintCodec readonly initialEncodedValue: NoInfer } } export const make = Effect.fnUntraced(function* ( options: make.Options ): Effect.fn.Return< MutationForm, never, Scope.Scope | RD | RE | MR > { return new MutationFormImpl( 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 const thenRun = ( self: Effect.Effect, E, R>, ): Effect.Effect< MutationForm, E, Scope.Scope | R > => Effect.tap( self, form => Effect.forkScoped(form.run), )