From a1dc98aa04ba2b659e67d4186084e491cb8e207f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 30 Sep 2025 19:43:39 +0200 Subject: [PATCH] Refactoring --- packages/effect-fc/src/Form.ts | 66 ++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/packages/effect-fc/src/Form.ts b/packages/effect-fc/src/Form.ts index d17b673..4f27696 100644 --- a/packages/effect-fc/src/Form.ts +++ b/packages/effect-fc/src/Form.ts @@ -1,5 +1,5 @@ import * as AsyncData from "@typed/async-data" -import { Array, Duration, Effect, Equal, Exit, flow, identity, Option, ParseResult, pipe, Pipeable, Ref, Schema, Scope, Stream, Subscribable, SubscriptionRef } from "effect" +import { Array, Cause, Chunk, Duration, Effect, Equal, Exit, Fiber, flow, identity, Option, ParseResult, pipe, Pipeable, Ref, Schema, Scope, Stream, Subscribable, SubscriptionRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as React from "react" import { Hooks } from "./hooks/index.js" @@ -19,7 +19,7 @@ extends Pipeable.Pipeable { readonly valueRef: SubscriptionRef.SubscriptionRef>, readonly encodedValueRef: SubscriptionRef.SubscriptionRef, readonly errorRef: SubscriptionRef.SubscriptionRef>, - readonly isValidatingRef: SubscriptionRef.SubscriptionRef + readonly validationFiberRef: SubscriptionRef.SubscriptionRef>> readonly submitStateRef: SubscriptionRef.SubscriptionRef>, readonly canSubmitSubscribable: Subscribable.Subscribable @@ -36,7 +36,7 @@ extends Pipeable.Class() implements Form { readonly valueRef: SubscriptionRef.SubscriptionRef>, readonly encodedValueRef: SubscriptionRef.SubscriptionRef, readonly errorRef: SubscriptionRef.SubscriptionRef>, - readonly isValidatingRef: SubscriptionRef.SubscriptionRef, + readonly validationFiberRef: SubscriptionRef.SubscriptionRef>>, readonly submitStateRef: SubscriptionRef.SubscriptionRef>, readonly canSubmitSubscribable: Subscribable.Subscribable, @@ -62,7 +62,7 @@ export const make: { ) { const valueRef = yield* SubscriptionRef.make(Option.none()) const errorRef = yield* SubscriptionRef.make(Option.none()) - const isValidatingRef = yield* SubscriptionRef.make(false) + const validationFiberRef = yield* SubscriptionRef.make(Option.none>()) const submitStateRef = yield* SubscriptionRef.make(AsyncData.noData()) return new FormImpl( @@ -72,25 +72,25 @@ export const make: { valueRef, yield* SubscriptionRef.make(options.initialEncodedValue), errorRef, - isValidatingRef, + validationFiberRef, submitStateRef, pipe( - ([value, error, isValidating, submitState]: readonly [ + ([value, error, validationFiber, submitState]: readonly [ Option.Option, Option.Option, - boolean, + Option.Option>, AsyncData.AsyncData, - ]) => Option.isSome(value) && Option.isNone(error) && !isValidating && !AsyncData.isLoading(submitState), + ]) => Option.isSome(value) && Option.isNone(error) && Option.isNone(validationFiber) && !AsyncData.isLoading(submitState), filter => SubscribableInternal.make({ - get: Effect.map(Effect.all([valueRef, errorRef, isValidatingRef, submitStateRef]), filter), + get: Effect.map(Effect.all([valueRef, errorRef, validationFiberRef, submitStateRef]), filter), get changes() { return Stream.map( Stream.zipLatestAll( valueRef.changes, errorRef.changes, - isValidatingRef.changes, + validationFiberRef.changes, submitStateRef.changes, ), filter, @@ -103,14 +103,38 @@ export const make: { export const run = ( self: Form -): Effect.Effect => Stream.runForEach( +): Effect.Effect => Stream.runForEach( self.encodedValueRef.changes, - encodedValue => SubscriptionRef.set(self.isValidatingRef, true).pipe( - Effect.andThen(Schema.decode(self.schema, { errors: "all" })(encodedValue)), - Effect.andThen(v => SubscriptionRef.set(self.valueRef, Option.some(v))), - Effect.andThen(SubscriptionRef.set(self.errorRef, Option.none())), - Effect.catchTag("ParseError", e => SubscriptionRef.set(self.errorRef, Option.some(e))), - Effect.andThen(SubscriptionRef.set(self.isValidatingRef, false)), + encodedValue => self.validationFiberRef.pipe( + Effect.andThen(Option.match({ + onSome: Fiber.interrupt, + onNone: () => Effect.void, + })), + Effect.andThen( + Effect.addFinalizer(() => SubscriptionRef.set(self.validationFiberRef, Option.none())).pipe( + Effect.andThen(Schema.decode(self.schema, { errors: "all" })(encodedValue)), + Effect.exit, + Effect.andThen(flow( + Exit.matchEffect({ + onSuccess: v => Effect.andThen( + SubscriptionRef.set(self.valueRef, Option.some(v)), + SubscriptionRef.set(self.errorRef, Option.none()), + ), + onFailure: c => Option.match( + Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), + { + onSome: e => SubscriptionRef.set(self.errorRef, Option.some(e)), + onNone: () => Effect.void, + }, + ), + }), + Effect.uninterruptible, + )), + Effect.scoped, + Effect.forkScoped, + ) + ), + Effect.andThen(fiber => SubscriptionRef.set(self.validationFiberRef, Option.some(fiber))) ), ) @@ -177,7 +201,13 @@ export const field = SubscribableInternal.make({ + get: Effect.map(self.validationFiberRef.get, filter), + get changes() { return Stream.map(self.validationFiberRef.changes, filter) }, + }), + ), pipe( AsyncData.isLoading,