import { Array, type Context, Effect, Equal, Fiber, Option, Pipeable, Predicate, Schema, SchemaIssue, SchemaParser, type Scope, Semaphore, Stream, SubscriptionRef, } from "effect" import * as Form from "./Form.js" import * as Lens from "./Lens.js" import * as Subscribable from "./Subscribable.js" type FormSchema = Schema.Top & { readonly Type: A readonly Encoded: I readonly DecodingServices: R readonly EncodingServices: R } export const SynchronizedFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/SynchronizedForm") export type SynchronizedFormTypeId = typeof SynchronizedFormTypeId export interface SynchronizedForm< in out A, in out I = A, in out R = never, in out TER = never, in out TEW = never, in out TRR = never, in out TRW = never, > extends Form.Form { readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId readonly schema: FormSchema readonly context: Context.Context readonly target: Lens.Lens readonly validationFiber: Subscribable.Subscribable>, never, never> readonly run: Effect.Effect } export class SynchronizedFormImpl< in out A, in out I = A, in out R = never, in out TER = never, in out TEW = never, in out TRR = never, in out TRW = never, > extends Pipeable.Class implements SynchronizedForm { readonly [Form.FormTypeId]: Form.FormTypeId = Form.FormTypeId readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId = SynchronizedFormTypeId readonly path = [] as const readonly value: Subscribable.Subscribable, TER, never> readonly encodedValue: Lens.Lens readonly isValidating: Subscribable.Subscribable readonly canCommit: Subscribable.Subscribable constructor( readonly schema: FormSchema, readonly context: Context.Context, readonly target: Lens.Lens, readonly internalEncodedValue: Lens.Lens, readonly issues: Lens.Lens, readonly validationFiber: Lens.Lens>, never, never, never, never>, readonly isCommitting: Lens.Lens, readonly runSemaphore: Semaphore.Semaphore, ) { super() this.value = Subscribable.make({ get: Effect.provide(Effect.map(target.get, Option.some), context), changes: Stream.provideContext( target.changes.pipe( Stream.map(Option.some), Stream.catchCause(() => Stream.make(Option.none())), ), context, ), }) 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, }) as unknown as Lens.Lens this.isValidating = Subscribable.map(validationFiber, Option.isSome) const commitState = Subscribable.zipLatestAll(issues as any, validationFiber as any, isCommitting as any) as unknown as Subscribable.Subscribable>, boolean, ]> this.canCommit = Subscribable.map( commitState, ([currentIssues, fiber, committing]) => currentIssues.length === 0 && Option.isNone(fiber) && !committing, ) } 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)) yield* Fiber.join(fiber).pipe( Effect.flatMap(value => Effect.ensuring( Effect.andThen( Lens.set(self.isCommitting, true), Effect.andThen(Lens.set(self.issues, Array.empty()), Lens.set(self.target, value)), ), Lens.set(self.isCommitting, false), )), Effect.catchIf(SchemaIssue.isIssue, issue => Lens.set(self.issues, formatIssue(issue))), ) }).pipe(Effect.provide(this.context)) as Effect.Effect } get run(): Effect.Effect { return this.runSemaphore.withPermits(1)(Effect.provide( Stream.runForEach(Stream.drop(this.target.changes, 1), targetValue => Effect.ignore( Effect.flatMap(SchemaParser.encodeEffect(this.schema)(targetValue), encodedValue => Effect.flatMap( Lens.get(this.internalEncodedValue), current => Equal.equals(encodedValue, current) ? Effect.succeed(undefined) : Effect.andThen( Lens.set(this.issues, Array.empty()), Lens.set(this.internalEncodedValue, encodedValue), ), )), )), this.context, )) } } 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 isSynchronizedForm = (u: unknown): u is SynchronizedForm => Predicate.hasProperty(u, SynchronizedFormTypeId) export declare namespace make { export interface Options { readonly schema: FormSchema readonly target: Lens.Lens readonly initialEncodedValue?: NoInfer } } export const make = Effect.fnUntraced(function* ( options: make.Options, ): Effect.fn.Return< SynchronizedForm, SchemaIssue.Issue | TER, Scope.Scope | R | TRR | TRW > { const initialEncodedValue = options.initialEncodedValue !== undefined ? options.initialEncodedValue : yield* Effect.flatMap(Lens.get(options.target), SchemaParser.encodeEffect(options.schema)) return new SynchronizedFormImpl( options.schema, yield* Effect.context(), options.target, Lens.fromSubscriptionRef(yield* SubscriptionRef.make(initialEncodedValue)), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Array.empty())), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none>())), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(false)), yield* Semaphore.make(1), ) }) export declare namespace service { export interface Options extends make.Options {} } export const service = ( options: service.Options, ): Effect.Effect< SynchronizedForm, SchemaIssue.Issue | TER, Scope.Scope | R | TRR | TRW > => Effect.tap(make(options), form => Effect.asVoid(Effect.forkScoped(form.run)))