Result work
Some checks failed
Lint / lint (push) Failing after 14s

This commit is contained in:
Julien Valverdé
2025-10-27 16:47:58 +01:00
parent 308025d662
commit 12878cd76b
3 changed files with 100 additions and 27 deletions

View File

@@ -1,9 +1,9 @@
import * as AsyncData from "@typed/async-data"
import { Array, Cause, Chunk, type Duration, Effect, Equal, Exit, Fiber, flow, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect" import { Array, Cause, Chunk, type Duration, Effect, Equal, Exit, Fiber, flow, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
import type { NoSuchElementException } from "effect/Cause" import type { NoSuchElementException } from "effect/Cause"
import * as React from "react" import * as React from "react"
import * as Component from "./Component.js" import * as Component from "./Component.js"
import * as PropertyPath from "./PropertyPath.js" import * as PropertyPath from "./PropertyPath.js"
import * as Result from "./Result.js"
import * as Subscribable from "./Subscribable.js" import * as Subscribable from "./Subscribable.js"
import * as SubscriptionRef from "./SubscriptionRef.js" import * as SubscriptionRef from "./SubscriptionRef.js"
import * as SubscriptionSubRef from "./SubscriptionSubRef.js" import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
@@ -25,7 +25,7 @@ extends Pipeable.Pipeable {
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I> readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>> readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>> readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>
readonly submitStateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<SA, SE>> readonly submitResultRef: SubscriptionRef.SubscriptionRef<Result.Result<SA, SE>>
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean> readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>
} }
@@ -44,7 +44,7 @@ extends Pipeable.Class() implements Form<A, I, R, SA, SE, SR> {
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>, readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>, readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>, readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>,
readonly submitStateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<SA, SE>>, readonly submitResultRef: SubscriptionRef.SubscriptionRef<Result.Result<SA, SE>>,
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>, readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>,
) { ) {
@@ -77,7 +77,7 @@ export const make: {
const valueRef = yield* SubscriptionRef.make(Option.none<A>()) const valueRef = yield* SubscriptionRef.make(Option.none<A>())
const errorRef = yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()) const errorRef = yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>())
const validationFiberRef = yield* SubscriptionRef.make(Option.none<Fiber.Fiber<void, never>>()) const validationFiberRef = yield* SubscriptionRef.make(Option.none<Fiber.Fiber<void, never>>())
const submitStateRef = yield* SubscriptionRef.make(AsyncData.noData<SA, SE>()) const submitResultRef = yield* SubscriptionRef.make<Result.Result<SA, SE>>(Result.initial())
return new FormImpl( return new FormImpl(
options.schema, options.schema,
@@ -89,15 +89,15 @@ export const make: {
yield* SubscriptionRef.make(options.initialEncodedValue), yield* SubscriptionRef.make(options.initialEncodedValue),
errorRef, errorRef,
validationFiberRef, validationFiberRef,
submitStateRef, submitResultRef,
Subscribable.map( Subscribable.map(
Subscribable.zipLatestAll(valueRef, errorRef, validationFiberRef, submitStateRef), Subscribable.zipLatestAll(valueRef, errorRef, validationFiberRef, submitResultRef),
([value, error, validationFiber, submitState]) => ( ([value, error, validationFiber, submitResult]) => (
Option.isSome(value) && Option.isSome(value) &&
Option.isNone(error) && Option.isNone(error) &&
Option.isNone(validationFiber) && Option.isNone(validationFiber) &&
!AsyncData.isLoading(submitState) (Result.isRunning(submitResult) || Result.isRefreshing(submitResult))
), ),
), ),
) )
@@ -198,11 +198,11 @@ export const field = <A, I, R, SA, SE, SR, const P extends PropertyPath.Paths<No
onNone: () => Effect.succeed([]), onNone: () => Effect.succeed([]),
})), })),
Subscribable.map(self.validationFiberRef, Option.isSome), Subscribable.map(self.validationFiberRef, Option.isSome),
Subscribable.map(self.submitStateRef, AsyncData.isLoading) Subscribable.map(self.submitResultRef, flow(Result.isRunning, Result.isRefreshing)),
) )
export const FormFieldTypeId: unique symbol = Symbol.for("effect-fc/FormField") export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField")
export type FormFieldTypeId = typeof FormFieldTypeId export type FormFieldTypeId = typeof FormFieldTypeId
export interface FormField<in out A, in out I = A> export interface FormField<in out A, in out I = A>

View File

@@ -1,8 +1,8 @@
import { Cause, Exit, Option, Pipeable, Predicate } from "effect" import { Cause, Effect, Equal, Exit, Hash, Match, Option, Pipeable, Predicate, pipe, Queue, type Scope } from "effect"
export const TypeId: unique symbol = Symbol.for("@effect-fc/Result/Result") export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
export type TypeId = typeof TypeId export type ResultTypeId = typeof ResultTypeId
export type Result<A, E = never, P = never> = ( export type Result<A, E = never, P = never> = (
| Initial | Initial
@@ -14,8 +14,8 @@ export type Result<A, E = never, P = never> = (
) )
export namespace Result { export namespace Result {
export interface Prototype extends Pipeable.Pipeable { export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
readonly [TypeId]: TypeId readonly [ResultTypeId]: ResultTypeId
} }
export type Success<R extends Result<any, any, any>> = [R] extends [Result<infer A, infer _E, infer _P>] ? A : never export type Success<R extends Result<any, any, any>> = [R] extends [Result<infer A, infer _E, infer _P>] ? A : never
@@ -51,20 +51,61 @@ export interface Refreshing<P = never> {
const ResultPrototype = Object.freeze({ const ResultPrototype = Object.freeze({
...Pipeable.Prototype, ...Pipeable.Prototype,
[TypeId]: TypeId, [ResultTypeId]: ResultTypeId,
[Equal.symbol](this: Result<any, any, any>, that: Result<any, any, any>): boolean {
if (this._tag !== that._tag)
return false
return Match.value(this).pipe(
Match.tag("Initial", () => true),
Match.tag("Running", self => Equal.equals(self.progress, (that as Running<any>).progress)),
Match.tag("Success", self =>
Equal.equals(self.value, (that as Success<any>).value) &&
(isRefreshing(self) ? self.refreshing : false) === (isRefreshing(that) ? that.refreshing : false) &&
Equal.equals(isRefreshing(self) ? self.progress : undefined, isRefreshing(that) ? that.progress : undefined)
),
Match.tag("Failure", self =>
Equal.equals(self.cause, (that as Failure<any, any>).cause) &&
(isRefreshing(self) ? self.refreshing : false) === (isRefreshing(that) ? that.refreshing : false) &&
Equal.equals(isRefreshing(self) ? self.progress : undefined, isRefreshing(that) ? that.progress : undefined)
),
Match.exhaustive,
)
},
[Hash.symbol](this: Result<any, any, any>): number {
const tagHash = Hash.string(this._tag)
return Match.value(this).pipe(
Match.tag("Initial", () => tagHash),
Match.tag("Running", self => Hash.combine(Hash.hash(self.progress))(tagHash)),
Match.tag("Success", self => pipe(tagHash,
Hash.combine(Hash.hash(self.value)),
Hash.combine(Hash.hash(isRefreshing(self) ? self.progress : undefined)),
)),
Match.tag("Failure", self => pipe(tagHash,
Hash.combine(Hash.hash(self.cause)),
Hash.combine(Hash.hash(isRefreshing(self) ? self.progress : undefined)),
)),
Match.exhaustive,
Hash.cached(this),
)
},
} as const satisfies Result.Prototype) } as const satisfies Result.Prototype)
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, TypeId) export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial" export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
export const isRunning = (u: unknown): u is Running => isResult(u) && u._tag === "Running"
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success" export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
export const isFailure = (u: unknown): u is Failure<unknown, unknown> => isResult(u) && u._tag === "Failure" export const isFailure = (u: unknown): u is Failure<unknown, unknown> => isResult(u) && u._tag === "Failure"
export const isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing") && u.refreshing export const isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing") && u.refreshing
export const initial = (): Initial => Object.setPrototypeOf({}, ResultPrototype) export const initial = (): Initial => Object.setPrototypeOf({}, ResultPrototype)
export const running = <P = never>(progress?: P): Running<P> => Object.setPrototypeOf({ progress }, ResultPrototype) export const running = <P = never>(progress?: P): Running<P> => Object.setPrototypeOf({ progress }, ResultPrototype)
export const success = <A>(value: A): Success<A> => Object.setPrototypeOf({ value }, ResultPrototype) export const succeed = <A>(value: A): Success<A> => Object.setPrototypeOf({ value }, ResultPrototype)
export const failure = <E, A = never>( export const fail = <E, A = never>(
cause: Cause.Cause<E>, cause: Cause.Cause<E>,
previousSuccess?: Success<A>, previousSuccess?: Success<A>,
): Failure<A, E> => Object.setPrototypeOf({ ): Failure<A, E> => Object.setPrototypeOf({
@@ -79,11 +120,12 @@ export const refreshing = <R extends Success<any> | Failure<any, any>, P = never
Object.getPrototypeOf(result), Object.getPrototypeOf(result),
) )
export const fromExit = <A, E>( export const fromExit = <A, E>(
exit: Exit.Exit<A, E> exit: Exit.Exit<A, E>
): Success<A> | Failure<A, E> => exit._tag === "Success" ): Success<A> | Failure<A, E> => exit._tag === "Success"
? success(exit.value) ? succeed(exit.value)
: failure(exit.cause) : fail(exit.cause)
export const toExit = <A, E, P>( export const toExit = <A, E, P>(
self: Result<A, E, P> self: Result<A, E, P>
@@ -97,3 +139,15 @@ export const toExit = <A, E, P>(
return Exit.fail(new Cause.NoSuchElementException()) return Exit.fail(new Cause.NoSuchElementException())
} }
} }
export const forkEffect = <A, E, R>(
effect: Effect.Effect<A, E, R>
): Effect.Effect<Queue.Dequeue<Result<A, E>>, never, Scope.Scope | R> => Queue.unbounded<Result<A, E>>().pipe(
Effect.tap(Queue.offer(initial())),
Effect.tap(queue => Effect.forkScoped(Queue.offer(queue, running()).pipe(
Effect.andThen(effect),
Effect.exit,
Effect.andThen(exit => Queue.offer(queue, fromExit(exit))),
Effect.andThen(Queue.shutdown(queue)),
))),
)

View File

@@ -4,16 +4,26 @@ import * as Component from "./Component.js"
import * as SetStateAction from "./SetStateAction.js" import * as SetStateAction from "./SetStateAction.js"
export namespace useSubscriptionRefState {
export interface Options<A> {
readonly equivalence?: Equivalence.Equivalence<A>
}
}
export const useSubscriptionRefState: { export const useSubscriptionRefState: {
<A>( <A>(
ref: SubscriptionRef.SubscriptionRef<A> ref: SubscriptionRef.SubscriptionRef<A>,
options?: useSubscriptionRefState.Options<NoInfer<A>>,
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>], never, Scope.Scope> ): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>], never, Scope.Scope>
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) { } = Effect.fnUntraced(function* <A>(
ref: SubscriptionRef.SubscriptionRef<A>,
options?: useSubscriptionRefState.Options<NoInfer<A>>,
) {
const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => ref)) const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => ref))
yield* Component.useReactEffect(() => Effect.forkScoped( yield* Component.useReactEffect(() => Effect.forkScoped(
Stream.runForEach( Stream.runForEach(
Stream.changesWith(ref.changes, Equivalence.strict()), Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
v => Effect.sync(() => setReactStateValue(v)), v => Effect.sync(() => setReactStateValue(v)),
) )
), [ref]) ), [ref])
@@ -28,14 +38,23 @@ export const useSubscriptionRefState: {
return [reactStateValue, setValue] return [reactStateValue, setValue]
}) })
export namespace useSubscriptionRefFromState {
export interface Options<A> {
readonly equivalence?: Equivalence.Equivalence<A>
}
}
export const useSubscriptionRefFromState: { export const useSubscriptionRefFromState: {
<A>(state: readonly [A, React.Dispatch<React.SetStateAction<A>>]): Effect.Effect<SubscriptionRef.SubscriptionRef<A>, never, Scope.Scope> <A>(
} = Effect.fnUntraced(function*([value, setValue]) { state: readonly [A, React.Dispatch<React.SetStateAction<A>>],
options?: useSubscriptionRefFromState.Options<NoInfer<A>>,
): Effect.Effect<SubscriptionRef.SubscriptionRef<A>, never, Scope.Scope>
} = Effect.fnUntraced(function*([value, setValue], options) {
const ref = yield* Component.useOnChange(() => Effect.tap( const ref = yield* Component.useOnChange(() => Effect.tap(
SubscriptionRef.make(value), SubscriptionRef.make(value),
ref => Effect.forkScoped( ref => Effect.forkScoped(
Stream.runForEach( Stream.runForEach(
Stream.changesWith(ref.changes, Equivalence.strict()), Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
v => Effect.sync(() => setValue(v)), v => Effect.sync(() => setValue(v)),
) )
), ),