diff --git a/packages/effect-fc/src/Form.ts b/packages/effect-fc/src/Form.ts index f36ff90..d854eef 100644 --- a/packages/effect-fc/src/Form.ts +++ b/packages/effect-fc/src/Form.ts @@ -1,9 +1,7 @@ -import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, Function, identity, Option, ParseResult, Pipeable, Predicate, Schema, type Scope, Stream } from "effect" +import { Array, type Cause, Chunk, type Duration, Effect, Equal, Function, identity, Option, type ParseResult, Pipeable, Predicate, type Scope, Stream } from "effect" import type * as React from "react" import * as Component from "./Component.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" @@ -42,379 +40,7 @@ extends Pipeable.Class() implements Form { } -export const SubmittableFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/SubmittableForm") -export type SubmittableFormTypeId = typeof SubmittableFormTypeId - -export interface SubmittableForm -extends 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 [FormTypeId]: FormTypeId = 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 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 { - readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId - - readonly schema: Schema.Schema - 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 [FormTypeId]: FormTypeId = FormTypeId - readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId = SynchronizedFormTypeId - - readonly path = [] as const - - constructor( - readonly schema: Schema.Schema, - readonly context: Context.Context, - readonly target: Lens.Lens, - - 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: Lens.Lens, - - readonly runSemaphore: Effect.Semaphore, - ) { - super() - } - - get run(): Effect.Effect { - return this.runSemaphore.withPermits(1)(Effect.provide( - Effect.all([ - 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 => Lens.set(this.value, Option.some(v)).pipe( - Effect.andThen(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.tap(value => Effect.onExit( - Effect.andThen( - Lens.set(this.isCommitting, true), - Lens.set(this.target, value), - ), - () => Lens.set(this.isCommitting, false), - )), - Effect.forkScoped, - ) - ), - ), - ), - - Stream.runForEach( - Stream.drop(this.target.changes, 1), - - targetValue => Schema.encode(this.schema, { errors: "all" })(targetValue).pipe( - Effect.flatMap(encodedValue => Effect.whenEffect( - Lens.set(this.encodedValue, encodedValue), - Effect.map( - Lens.get(this.encodedValue), - currentEncodedValue => !Equal.equals(encodedValue, currentEncodedValue), - ), - )), - ), - ), - ], { concurrency: "unbounded", discard: true }), - - this.context, - )) - } -} - - export const isForm = (u: unknown): u is Form => Predicate.hasProperty(u, FormTypeId) -export const isSubmittableForm = (u: unknown): u is SubmittableForm => Predicate.hasProperty(u, SubmittableFormTypeId) -export const isSynchronizedForm = (u: unknown): u is SynchronizedForm => Predicate.hasProperty(u, SynchronizedFormTypeId) - -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), -) - -export declare namespace makeSynchronized { - export interface Options { - readonly schema: Schema.Schema - readonly target: Lens.Lens - readonly initialEncodedValue: NoInfer - } -} - -export const makeSynchronized = Effect.fnUntraced(function* ( - options: makeSynchronized.Options -): Effect.fn.Return< - SynchronizedForm, - ParseResult.ParseError | TER, - Scope.Scope | R | TRR | TRW -> { - 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>())) - const isCommittingLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(false)) - const initialEncodedValue = yield* Lens.get(options.target).pipe( - Effect.flatMap(Schema.encode(options.schema, { errors: "all" })), - ) - - return new SynchronizedFormImpl( - options.schema, - yield* Effect.context(), - options.target, - - valueLens, - Lens.fromSubscriptionRef(yield* SubscriptionRef.make(initialEncodedValue)), - issuesLens, - validationFiberLens, - Subscribable.map(validationFiberLens, Option.isSome), - - Subscribable.map( - Subscribable.zipLatestAll(valueLens, issuesLens, validationFiberLens, isCommittingLens), - ([value, issues, validationFiber, isCommitting]) => ( - Option.isSome(value) && - Array.isEmptyReadonlyArray(issues) && - Option.isNone(validationFiber) && - !isCommitting - ), - ), - isCommittingLens, - - yield* Effect.makeSemaphore(1), - ) -}) - -export declare namespace serviceSynchronized { - export interface Options - extends makeSynchronized.Options {} -} - -export const serviceSynchronized = ( - options: serviceSynchronized.Options -): Effect.Effect< - SynchronizedForm, - ParseResult.ParseError | TER, - Scope.Scope | R | TRR | TRW -> => Effect.tap( - makeSynchronized(options), - form => Effect.forkScoped(form.run), -) const filterIssuesByPath = ( diff --git a/packages/effect-fc/src/SubmittableForm.ts b/packages/effect-fc/src/SubmittableForm.ts new file mode 100644 index 0000000..219e536 --- /dev/null +++ b/packages/effect-fc/src/SubmittableForm.ts @@ -0,0 +1,194 @@ +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), +) diff --git a/packages/effect-fc/src/SynchronizedForm.ts b/packages/effect-fc/src/SynchronizedForm.ts new file mode 100644 index 0000000..785dd09 --- /dev/null +++ b/packages/effect-fc/src/SynchronizedForm.ts @@ -0,0 +1,194 @@ +import { Array, Cause, Chunk, type Context, Effect, Equal, Exit, Fiber, Option, ParseResult, Pipeable, Predicate, Schema, type Scope, Stream } from "effect" +import * as Form from "./Form.js" +import * as Lens from "./Lens.js" +import * as Subscribable from "./Subscribable.js" +import * as SubscriptionRef from "./SubscriptionRef.js" + + +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: Schema.Schema + 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 + + constructor( + readonly schema: Schema.Schema, + readonly context: Context.Context, + readonly target: Lens.Lens, + + 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: Lens.Lens, + + readonly runSemaphore: Effect.Semaphore, + ) { + super() + } + + get run(): Effect.Effect { + return this.runSemaphore.withPermits(1)(Effect.provide( + Effect.all([ + 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 => Lens.set(this.value, Option.some(v)).pipe( + Effect.andThen(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.tap(value => Effect.onExit( + Effect.andThen( + Lens.set(this.isCommitting, true), + Lens.set(this.target, value), + ), + () => Lens.set(this.isCommitting, false), + )), + Effect.forkScoped, + ) + ), + ), + ), + + Stream.runForEach( + Stream.drop(this.target.changes, 1), + + targetValue => Schema.encode(this.schema, { errors: "all" })(targetValue).pipe( + Effect.flatMap(encodedValue => Effect.whenEffect( + Lens.set(this.encodedValue, encodedValue), + Effect.map( + Lens.get(this.encodedValue), + currentEncodedValue => !Equal.equals(encodedValue, currentEncodedValue), + ), + )), + ), + ), + ], { concurrency: "unbounded", discard: true }), + + this.context, + )) + } +} + + +export const isSynchronizedForm = (u: unknown): u is SynchronizedForm => Predicate.hasProperty(u, SynchronizedFormTypeId) + +export declare namespace makeSynchronized { + export interface Options { + readonly schema: Schema.Schema + readonly target: Lens.Lens + readonly initialEncodedValue: NoInfer + } +} + +export const makeSynchronized = Effect.fnUntraced(function* ( + options: makeSynchronized.Options +): Effect.fn.Return< + SynchronizedForm, + ParseResult.ParseError | TER, + Scope.Scope | R | TRR | TRW +> { + 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>())) + const isCommittingLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(false)) + const initialEncodedValue = yield* Lens.get(options.target).pipe( + Effect.flatMap(Schema.encode(options.schema, { errors: "all" })), + ) + + return new SynchronizedFormImpl( + options.schema, + yield* Effect.context(), + options.target, + + valueLens, + Lens.fromSubscriptionRef(yield* SubscriptionRef.make(initialEncodedValue)), + issuesLens, + validationFiberLens, + Subscribable.map(validationFiberLens, Option.isSome), + + Subscribable.map( + Subscribable.zipLatestAll(valueLens, issuesLens, validationFiberLens, isCommittingLens), + ([value, issues, validationFiber, isCommitting]) => ( + Option.isSome(value) && + Array.isEmptyReadonlyArray(issues) && + Option.isNone(validationFiber) && + !isCommitting + ), + ), + isCommittingLens, + + yield* Effect.makeSemaphore(1), + ) +}) + +export declare namespace serviceSynchronized { + export interface Options + extends makeSynchronized.Options {} +} + +export const serviceSynchronized = ( + options: serviceSynchronized.Options +): Effect.Effect< + SynchronizedForm, + ParseResult.ParseError | TER, + Scope.Scope | R | TRR | TRW +> => Effect.tap( + makeSynchronized(options), + form => Effect.forkScoped(form.run), +) diff --git a/packages/effect-fc/src/index.ts b/packages/effect-fc/src/index.ts index 8dbe743..131cf54 100644 --- a/packages/effect-fc/src/index.ts +++ b/packages/effect-fc/src/index.ts @@ -13,6 +13,8 @@ export * as ReactRuntime from "./ReactRuntime.js" export * as Result from "./Result.js" export * as SetStateAction from "./SetStateAction.js" export * as Stream from "./Stream.js" +export * as SubmittableForm from "./SubmittableForm.js" export * as Subscribable from "./Subscribable.js" export * as SubscriptionRef from "./SubscriptionRef.js" export * as SubscriptionSubRef from "./SubscriptionSubRef.js" +export * as SynchronizedForm from "./SynchronizedForm.js"