0.2.1 (#26)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud> Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "effect-fc",
|
||||
"description": "Write React function components with Effect",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
@@ -38,11 +38,8 @@
|
||||
"clean:modules": "rm -rf node_modules"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"effect": "^3.15.0",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typed/async-data": "^0.13.1"
|
||||
"@types/react": "^19.2.0",
|
||||
"effect": "^3.19.0",
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** biome-ignore-all lint/complexity/noBannedTypes: {} is the default type for React props */
|
||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Types, type Utils } from "effect"
|
||||
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Utils } from "effect"
|
||||
import * as React from "react"
|
||||
import { Memoized } from "./index.js"
|
||||
|
||||
@@ -29,7 +29,7 @@ extends
|
||||
): (props: P) => A
|
||||
}
|
||||
|
||||
export namespace Component {
|
||||
export declare namespace Component {
|
||||
export type Props<T extends Component<any, any, any, any>> = [T] extends [Component<infer P, infer _A, infer _E, infer _R>] ? P : never
|
||||
export type Success<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer A, infer _E, infer _R>] ? A : never
|
||||
export type Error<T extends Component<any, any, any, any>> = [T] extends [Component<infer _P, infer _A, infer E, infer _R>] ? E : never
|
||||
@@ -93,7 +93,7 @@ const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||
|
||||
export const isComponent = (u: unknown): u is Component<{}, React.ReactNode, unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export namespace make {
|
||||
export declare namespace make {
|
||||
export type Gen = {
|
||||
<Eff extends Utils.YieldWrap<Effect.Effect<any, any, any>>, A extends React.ReactNode, P extends {} = {}>(
|
||||
body: (props: P) => Generator<Eff, A, never>
|
||||
@@ -386,9 +386,9 @@ export const withOptions: {
|
||||
export const withRuntime: {
|
||||
<P extends {}, A extends React.ReactNode, E, R>(
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): (self: Component<P, A, E, Types.NoInfer<R>>) => (props: P) => A
|
||||
): (self: Component<P, A, E, Scope.Scope | NoInfer<R>>) => (props: P) => A
|
||||
<P extends {}, A extends React.ReactNode, E, R>(
|
||||
self: Component<P, A, E, Types.NoInfer<R>>,
|
||||
self: Component<P, A, E, Scope.Scope | NoInfer<R>>,
|
||||
context: React.Context<Runtime.Runtime<R>>,
|
||||
): (props: P) => A
|
||||
} = Function.dual(2, <P extends {}, A extends React.ReactNode, E, R>(
|
||||
@@ -402,11 +402,11 @@ export const withRuntime: {
|
||||
})
|
||||
|
||||
|
||||
export class ScopeMap extends Effect.Service<ScopeMap>()("effect-fc/Component/ScopeMap", {
|
||||
export class ScopeMap extends Effect.Service<ScopeMap>()("@effect-fc/Component/ScopeMap", {
|
||||
effect: Effect.bind(Effect.Do, "ref", () => Ref.make(HashMap.empty<object, ScopeMap.Entry>()))
|
||||
}) {}
|
||||
|
||||
export namespace ScopeMap {
|
||||
export declare namespace ScopeMap {
|
||||
export interface Entry {
|
||||
readonly scope: Scope.CloseableScope
|
||||
readonly closeFiber: Option.Option<Fiber.RuntimeFiber<void>>
|
||||
@@ -414,19 +414,17 @@ export namespace ScopeMap {
|
||||
}
|
||||
|
||||
|
||||
export namespace useScope {
|
||||
export declare namespace useScope {
|
||||
export interface Options {
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
readonly finalizerExecutionDebounce?: Duration.DurationInput
|
||||
}
|
||||
}
|
||||
|
||||
export const useScope: {
|
||||
(
|
||||
deps: React.DependencyList,
|
||||
options?: useScope.Options,
|
||||
): Effect.Effect<Scope.Scope>
|
||||
} = Effect.fnUntraced(function*(deps, options) {
|
||||
export const useScope = Effect.fnUntraced(function*(
|
||||
deps: React.DependencyList,
|
||||
options?: useScope.Options,
|
||||
): Effect.fn.Return<Scope.Scope> {
|
||||
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||
const runtimeRef = React.useRef<Runtime.Runtime<never>>(null!)
|
||||
runtimeRef.current = yield* Effect.runtime()
|
||||
@@ -478,32 +476,22 @@ export const useScope: {
|
||||
return scope
|
||||
})
|
||||
|
||||
export const useOnMount: {
|
||||
<A, E, R>(
|
||||
f: () => Effect.Effect<A, E, R>
|
||||
): Effect.Effect<A, E, R>
|
||||
} = Effect.fnUntraced(function* <A, E, R>(
|
||||
export const useOnMount = Effect.fnUntraced(function* <A, E, R>(
|
||||
f: () => Effect.Effect<A, E, R>
|
||||
) {
|
||||
): Effect.fn.Return<A, E, R> {
|
||||
const runtime = yield* Effect.runtime<R>()
|
||||
return yield* React.useState(() => Runtime.runSync(runtime)(Effect.cached(f())))[0]
|
||||
})
|
||||
|
||||
export namespace useOnChange {
|
||||
export type Options = useScope.Options
|
||||
export declare namespace useOnChange {
|
||||
export interface Options extends useScope.Options {}
|
||||
}
|
||||
|
||||
export const useOnChange: {
|
||||
<A, E, R>(
|
||||
f: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
options?: useOnChange.Options,
|
||||
): Effect.Effect<A, E, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <A, E, R>(
|
||||
export const useOnChange = Effect.fnUntraced(function* <A, E, R>(
|
||||
f: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
options?: useOnChange.Options,
|
||||
) {
|
||||
): Effect.fn.Return<A, E, Exclude<R, Scope.Scope>> {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
const scope = yield* useScope(deps, options)
|
||||
|
||||
@@ -513,24 +501,18 @@ export const useOnChange: {
|
||||
), [scope])
|
||||
})
|
||||
|
||||
export namespace useReactEffect {
|
||||
export declare namespace useReactEffect {
|
||||
export interface Options {
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
}
|
||||
}
|
||||
|
||||
export const useReactEffect: {
|
||||
<E, R>(
|
||||
f: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: useReactEffect.Options,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R>(
|
||||
export const useReactEffect = Effect.fnUntraced(function* <E, R>(
|
||||
f: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: useReactEffect.Options,
|
||||
) {
|
||||
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
||||
React.useEffect(() => runReactEffect(runtime, f, options), deps)
|
||||
@@ -558,35 +540,36 @@ const runReactEffect = <E, R>(
|
||||
Runtime.runSync(runtime),
|
||||
)
|
||||
|
||||
export namespace useReactLayoutEffect {
|
||||
export type Options = useReactEffect.Options
|
||||
export declare namespace useReactLayoutEffect {
|
||||
export interface Options extends useReactEffect.Options {}
|
||||
}
|
||||
|
||||
export const useReactLayoutEffect: {
|
||||
<E, R>(
|
||||
f: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: useReactLayoutEffect.Options,
|
||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||
} = Effect.fnUntraced(function* <E, R>(
|
||||
export const useReactLayoutEffect = Effect.fnUntraced(function* <E, R>(
|
||||
f: () => Effect.Effect<void, E, R>,
|
||||
deps?: React.DependencyList,
|
||||
options?: useReactLayoutEffect.Options,
|
||||
) {
|
||||
): Effect.fn.Return<void, never, Exclude<R, Scope.Scope>> {
|
||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: use of React.DependencyList
|
||||
React.useLayoutEffect(() => runReactEffect(runtime, f, options), deps)
|
||||
})
|
||||
|
||||
export const useCallbackSync: {
|
||||
<Args extends unknown[], A, E, R>(
|
||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<(...args: Args) => A, never, R>
|
||||
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||
export const useRunSync = <R = never>(): Effect.Effect<
|
||||
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => A,
|
||||
never,
|
||||
Scope.Scope | R
|
||||
> => Effect.andThen(Effect.runtime(), Runtime.runSync)
|
||||
|
||||
export const useRunPromise = <R = never>(): Effect.Effect<
|
||||
<A, E = never>(effect: Effect.Effect<A, E, Scope.Scope | R>) => Promise<A>,
|
||||
never,
|
||||
Scope.Scope | R
|
||||
> => Effect.andThen(Effect.runtime(), context => Runtime.runPromise(context))
|
||||
|
||||
export const useCallbackSync = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
): Effect.fn.Return<(...args: Args) => A, never, R> {
|
||||
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
||||
runtimeRef.current = yield* Effect.runtime<R>()
|
||||
@@ -595,15 +578,10 @@ export const useCallbackSync: {
|
||||
return React.useCallback((...args: Args) => Runtime.runSync(runtimeRef.current)(f(...args)), deps)
|
||||
})
|
||||
|
||||
export const useCallbackPromise: {
|
||||
<Args extends unknown[], A, E, R>(
|
||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
||||
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||
export const useCallbackPromise = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||
f: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
): Effect.fn.Return<(...args: Args) => Promise<A>, never, R> {
|
||||
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||
const runtimeRef = React.useRef<Runtime.Runtime<R>>(null!)
|
||||
runtimeRef.current = yield* Effect.runtime<R>()
|
||||
@@ -612,26 +590,16 @@ export const useCallbackPromise: {
|
||||
return React.useCallback((...args: Args) => Runtime.runPromise(runtimeRef.current)(f(...args)), deps)
|
||||
})
|
||||
|
||||
export namespace useContext {
|
||||
export type Options = useScope.Options
|
||||
export declare namespace useContext {
|
||||
export interface Options extends useOnChange.Options {}
|
||||
}
|
||||
|
||||
export const useContext: {
|
||||
<ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>,
|
||||
options?: useContext.Options,
|
||||
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
||||
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
||||
export const useContext = <ROut, E, RIn>(
|
||||
layer: Layer.Layer<ROut, E, RIn>,
|
||||
options?: useContext.Options,
|
||||
) {
|
||||
const scope = yield* useScope([layer], options)
|
||||
|
||||
return yield* useOnChange(() => Effect.context<RIn>().pipe(
|
||||
Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
|
||||
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
|
||||
Effect.andThen(runtime => runtime.runtimeEffect),
|
||||
Effect.andThen(runtime => runtime.context),
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
), [scope])
|
||||
})
|
||||
): Effect.Effect<Context.Context<ROut>, E, Scope.Scope | RIn> => useOnChange(() => Effect.context<RIn>().pipe(
|
||||
Effect.map(context => ManagedRuntime.make(Layer.provide(layer, Layer.succeedContext(context)))),
|
||||
Effect.tap(runtime => Effect.addFinalizer(() => runtime.disposeEffect)),
|
||||
Effect.andThen(runtime => runtime.runtimeEffect),
|
||||
Effect.andThen(runtime => runtime.context),
|
||||
), [layer], options)
|
||||
|
||||
62
packages/effect-fc/src/ErrorObserver.ts
Normal file
62
packages/effect-fc/src/ErrorObserver.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { type Cause, Context, Effect, Exit, Layer, Option, Pipeable, Predicate, PubSub, type Queue, type Scope, Supervisor } from "effect"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/ErrorObserver/ErrorObserver")
|
||||
export type TypeId = typeof TypeId
|
||||
|
||||
export interface ErrorObserver<in out E = never> extends Pipeable.Pipeable {
|
||||
readonly [TypeId]: TypeId
|
||||
handle<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||
}
|
||||
|
||||
export const ErrorObserver = <E = never>(): Context.Tag<ErrorObserver, ErrorObserver<E>> => Context.GenericTag("@effect-fc/ErrorObserver/ErrorObserver")
|
||||
|
||||
class ErrorObserverImpl<in out E = never>
|
||||
extends Pipeable.Class() implements ErrorObserver<E> {
|
||||
readonly [TypeId]: TypeId = TypeId
|
||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||
|
||||
constructor(
|
||||
readonly pubsub: PubSub.PubSub<Cause.Cause<E>>
|
||||
) {
|
||||
super()
|
||||
this.subscribe = pubsub.subscribe
|
||||
}
|
||||
|
||||
handle<A, EffE, R>(effect: Effect.Effect<A, EffE, R>): Effect.Effect<A, EffE, R> {
|
||||
return Effect.tapErrorCause(effect, cause => PubSub.publish(this.pubsub, cause as Cause.Cause<E>))
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
||||
readonly value = Effect.void
|
||||
constructor(readonly pubsub: PubSub.PubSub<Cause.Cause<never>>) {
|
||||
super()
|
||||
}
|
||||
|
||||
onEnd<A, E>(_value: Exit.Exit<A, E>): void {
|
||||
if (Exit.isFailure(_value)) {
|
||||
Effect.runSync(PubSub.publish(this.pubsub, _value.cause as Cause.Cause<never>))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const isErrorObserver = (u: unknown): u is ErrorObserver<unknown> => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export const layer: Layer.Layer<ErrorObserver> = Layer.unwrapEffect(Effect.map(
|
||||
PubSub.unbounded<Cause.Cause<never>>(),
|
||||
pubsub => Layer.merge(
|
||||
Supervisor.addSupervisor(new ErrorObserverSupervisorImpl(pubsub)),
|
||||
Layer.succeed(ErrorObserver(), new ErrorObserverImpl(pubsub)),
|
||||
),
|
||||
))
|
||||
|
||||
export const handle = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => Effect.andThen(
|
||||
Effect.serviceOption(ErrorObserver()),
|
||||
Option.match({
|
||||
onSome: observer => observer.handle(effect),
|
||||
onNone: () => effect,
|
||||
}),
|
||||
)
|
||||
@@ -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 type { NoSuchElementException } from "effect/Cause"
|
||||
import * as React from "react"
|
||||
import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
|
||||
import type * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
import * as Mutation from "./Mutation.js"
|
||||
import * as PropertyPath from "./PropertyPath.js"
|
||||
import * as Result from "./Result.js"
|
||||
import * as Subscribable from "./Subscribable.js"
|
||||
import * as SubscriptionRef from "./SubscriptionRef.js"
|
||||
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||
@@ -12,208 +12,225 @@ import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
||||
export type FormTypeId = typeof FormTypeId
|
||||
|
||||
export interface Form<in out A, in out I = A, out R = never, in out SA = void, in out SE = A, out SR = never>
|
||||
export interface Form<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||
extends Pipeable.Pipeable {
|
||||
readonly [FormTypeId]: FormTypeId
|
||||
|
||||
readonly schema: Schema.Schema<A, I, R>
|
||||
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>
|
||||
readonly context: Context.Context<Scope.Scope | R>
|
||||
readonly mutation: Mutation.Mutation<
|
||||
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
||||
MA, ME, MR, MP
|
||||
>
|
||||
readonly autosubmit: boolean
|
||||
readonly debounce: Option.Option<Duration.DurationInput>
|
||||
|
||||
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>
|
||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>
|
||||
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>
|
||||
readonly submitStateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<SA, SE>>
|
||||
readonly value: Subscribable.Subscribable<Option.Option<A>>
|
||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
||||
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
|
||||
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
|
||||
|
||||
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>
|
||||
readonly canSubmit: Subscribable.Subscribable<boolean>
|
||||
|
||||
field<const P extends PropertyPath.Paths<I>>(
|
||||
path: P
|
||||
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>>
|
||||
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
|
||||
}
|
||||
|
||||
class FormImpl<in out A, in out I = A, out R = never, in out SA = void, in out SE = A, out SR = never>
|
||||
extends Pipeable.Class() implements Form<A, I, R, SA, SE, SR> {
|
||||
export class FormImpl<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||
extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
||||
readonly [FormTypeId]: FormTypeId = FormTypeId
|
||||
|
||||
constructor(
|
||||
readonly schema: Schema.Schema<A, I, R>,
|
||||
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>,
|
||||
readonly context: Context.Context<Scope.Scope | R>,
|
||||
readonly mutation: Mutation.Mutation<
|
||||
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
||||
MA, ME, MR, MP
|
||||
>,
|
||||
readonly autosubmit: boolean,
|
||||
readonly debounce: Option.Option<Duration.DurationInput>,
|
||||
|
||||
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>,
|
||||
readonly submitStateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<SA, SE>>,
|
||||
readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
||||
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
|
||||
|
||||
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>,
|
||||
readonly runSemaphore: Effect.Semaphore,
|
||||
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.canSubmit = Subscribable.map(
|
||||
Subscribable.zipLatestAll(this.value, this.error, this.validationFiber, this.mutation.result),
|
||||
([value, error, validationFiber, result]) => (
|
||||
Option.isSome(value) &&
|
||||
Option.isNone(error) &&
|
||||
Option.isNone(validationFiber) &&
|
||||
!(Result.isRunning(result) || Result.isRefreshing(result))
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
field<const P extends PropertyPath.Paths<I>>(
|
||||
path: P
|
||||
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>> {
|
||||
return this.fieldCache.pipe(
|
||||
Effect.map(HashMap.get(new FormFieldKey(path))),
|
||||
Effect.flatMap(Option.match({
|
||||
onSome: v => Effect.succeed(v as FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>),
|
||||
onNone: () => Effect.tap(
|
||||
Effect.succeed(makeFormField(this as Form<A, I, R, MA, ME, MR, MP>, path)),
|
||||
v => Ref.update(this.fieldCache, HashMap.set(new FormFieldKey(path), v as FormField<unknown, unknown>)),
|
||||
),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>
|
||||
|
||||
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
||||
return this.value.pipe(
|
||||
Effect.andThen(identity),
|
||||
Effect.andThen(value => this.submitValue(value)),
|
||||
)
|
||||
}
|
||||
submitValue(value: A): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>> {
|
||||
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<ParseResult.ParseError>),
|
||||
e => e._tag === "ParseError",
|
||||
),
|
||||
{
|
||||
onSome: e => Ref.set(this.error, Option.some(e)),
|
||||
onNone: () => Effect.void,
|
||||
},
|
||||
)
|
||||
: Effect.void
|
||||
),
|
||||
this.canSubmit.get,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
||||
|
||||
export namespace make {
|
||||
export interface Options<in out A, in out I, in out R, in out SA = void, in out SE = A, out SR = never> {
|
||||
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||
extends Mutation.make.Options<
|
||||
readonly [value: NoInfer<A>, form: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
|
||||
MA, ME, MR, MP
|
||||
> {
|
||||
readonly schema: Schema.Schema<A, I, R>
|
||||
readonly initialEncodedValue: NoInfer<I>
|
||||
readonly onSubmit: (
|
||||
this: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>,
|
||||
value: NoInfer<A>,
|
||||
) => Effect.Effect<SA, SE, SR>
|
||||
readonly autosubmit?: boolean
|
||||
readonly debounce?: Duration.DurationInput
|
||||
}
|
||||
}
|
||||
|
||||
export const make: {
|
||||
<A, I = A, R = never, SA = void, SE = A, SR = never>(
|
||||
options: make.Options<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<Form<A, I, R, SA, SE, SR>>
|
||||
} = Effect.fnUntraced(function* <A, I = A, R = never, SA = void, SE = A, SR = never>(
|
||||
options: make.Options<A, I, R, SA, SE, SR>
|
||||
) {
|
||||
const valueRef = yield* SubscriptionRef.make(Option.none<A>())
|
||||
const errorRef = yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>())
|
||||
const validationFiberRef = yield* SubscriptionRef.make(Option.none<Fiber.Fiber<void, never>>())
|
||||
const submitStateRef = yield* SubscriptionRef.make(AsyncData.noData<SA, SE>())
|
||||
|
||||
export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
||||
options: make.Options<A, I, R, MA, ME, MR, MP>
|
||||
): Effect.fn.Return<
|
||||
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
||||
never,
|
||||
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
||||
> {
|
||||
return new FormImpl(
|
||||
options.schema,
|
||||
options.onSubmit,
|
||||
yield* Effect.context<Scope.Scope | R>(),
|
||||
yield* Mutation.make(options),
|
||||
options.autosubmit ?? false,
|
||||
Option.fromNullable(options.debounce),
|
||||
|
||||
valueRef,
|
||||
yield* SubscriptionRef.make(Option.none<A>()),
|
||||
yield* SubscriptionRef.make(options.initialEncodedValue),
|
||||
errorRef,
|
||||
validationFiberRef,
|
||||
submitStateRef,
|
||||
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
|
||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()),
|
||||
|
||||
Subscribable.map(
|
||||
Subscribable.zipLatestAll(valueRef, errorRef, validationFiberRef, submitStateRef),
|
||||
([value, error, validationFiber, submitState]) => (
|
||||
Option.isSome(value) &&
|
||||
Option.isNone(error) &&
|
||||
Option.isNone(validationFiber) &&
|
||||
!AsyncData.isLoading(submitState)
|
||||
),
|
||||
),
|
||||
yield* Effect.makeSemaphore(1),
|
||||
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
|
||||
)
|
||||
})
|
||||
|
||||
export const run = <A, I, R, SA, SE, SR>(
|
||||
self: Form<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<void, never, Scope.Scope | R | SR> => Stream.runForEach(
|
||||
self.encodedValueRef.changes.pipe(
|
||||
Option.isSome(self.debounce) ? Stream.debounce(self.debounce.value) : identity
|
||||
),
|
||||
export const run = <A, I, R, MA, ME, MR, MP>(
|
||||
self: Form<A, I, R, MA, ME, MR, MP>
|
||||
): Effect.Effect<void> => {
|
||||
const _self = self as FormImpl<A, I, R, MA, ME, MR, MP>
|
||||
return _self.runSemaphore.withPermits(1)(Stream.runForEach(
|
||||
_self.encodedValue.changes.pipe(
|
||||
Option.isSome(_self.debounce) ? Stream.debounce(_self.debounce.value) : identity
|
||||
),
|
||||
|
||||
encodedValue => self.validationFiberRef.pipe(
|
||||
Effect.andThen(Option.match({
|
||||
onSome: Fiber.interrupt,
|
||||
onNone: () => Effect.void,
|
||||
})),
|
||||
Effect.andThen(
|
||||
Effect.addFinalizer(() => Ref.set(self.validationFiberRef, Option.none())).pipe(
|
||||
Effect.andThen(Schema.decode(self.schema, { errors: "all" })(encodedValue)),
|
||||
Effect.exit,
|
||||
Effect.andThen(flow(
|
||||
Exit.matchEffect({
|
||||
onSuccess: v => Ref.set(self.valueRef, Option.some(v)).pipe(
|
||||
Effect.andThen(Ref.set(self.errorRef, Option.none())),
|
||||
Effect.as(Option.some(v)),
|
||||
),
|
||||
onFailure: c => Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError").pipe(
|
||||
Option.match({
|
||||
onSome: e => Ref.set(self.errorRef, Option.some(e)),
|
||||
encodedValue => _self.validationFiber.pipe(
|
||||
Effect.andThen(Option.match({
|
||||
onSome: Fiber.interrupt,
|
||||
onNone: () => Effect.void,
|
||||
})),
|
||||
Effect.andThen(
|
||||
Effect.forkScoped(Effect.onExit(
|
||||
Schema.decode(_self.schema, { errors: "all" })(encodedValue),
|
||||
exit => Effect.andThen(
|
||||
Exit.matchEffect(exit, {
|
||||
onSuccess: v => Effect.andThen(
|
||||
Ref.set(_self.value, Option.some(v)),
|
||||
Ref.set(_self.error, Option.none()),
|
||||
),
|
||||
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
||||
onSome: e => Ref.set(_self.error, Option.some(e)),
|
||||
onNone: () => Effect.void,
|
||||
}),
|
||||
Effect.as(Option.none<A>()),
|
||||
),
|
||||
}),
|
||||
Effect.uninterruptible,
|
||||
)),
|
||||
Effect.scoped,
|
||||
|
||||
Effect.andThen(value => Option.isSome(value) && self.autosubmit
|
||||
? Effect.asVoid(Effect.forkScoped(submit(self)))
|
||||
: Effect.void
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
}),
|
||||
Ref.set(_self.validationFiber, Option.none()),
|
||||
),
|
||||
)).pipe(
|
||||
Effect.tap(fiber => Ref.set(_self.validationFiber, Option.some(fiber))),
|
||||
Effect.andThen(Fiber.join),
|
||||
Effect.andThen(value => _self.autosubmit
|
||||
? Effect.asVoid(Effect.forkScoped(_self.submitValue(value)))
|
||||
: Effect.void
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
),
|
||||
Effect.provide(_self.context),
|
||||
),
|
||||
Effect.andThen(fiber => Ref.set(self.validationFiberRef, Option.some(fiber)))
|
||||
),
|
||||
)
|
||||
|
||||
export const submit = <A, I, R, SA, SE, SR>(
|
||||
self: Form<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<Option.Option<AsyncData.AsyncData<SA, SE>>, NoSuchElementException, SR> => Effect.whenEffect(
|
||||
self.valueRef.pipe(
|
||||
Effect.andThen(identity),
|
||||
Effect.tap(Ref.set(self.submitStateRef, AsyncData.loading())),
|
||||
Effect.andThen(flow(
|
||||
self.onSubmit as (value: NoInfer<A>) => Effect.Effect<SA, SE | ParseResult.ParseError, SR>,
|
||||
Effect.tapErrorTag("ParseError", e => Ref.set(self.errorRef, Option.some(e as ParseResult.ParseError))),
|
||||
Effect.exit,
|
||||
Effect.map(Exit.match({
|
||||
onSuccess: a => AsyncData.success(a),
|
||||
onFailure: e => AsyncData.failure(e as Cause.Cause<SE>),
|
||||
})),
|
||||
Effect.tap(v => Ref.set(self.submitStateRef, v)),
|
||||
)),
|
||||
),
|
||||
|
||||
self.canSubmitSubscribable.get,
|
||||
)
|
||||
|
||||
export namespace service {
|
||||
export interface Options<in out A, in out I, in out R, in out SA = void, in out SE = A, out SR = never>
|
||||
extends make.Options<A, I, R, SA, SE, SR> {}
|
||||
))
|
||||
}
|
||||
|
||||
export const service = <A, I = A, R = never, SA = void, SE = A, SR = never>(
|
||||
options: service.Options<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<Form<A, I, R, SA, SE, SR>, never, Scope.Scope | R | SR> => Effect.tap(
|
||||
export namespace service {
|
||||
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||
extends make.Options<A, I, R, MA, ME, MR, MP> {}
|
||||
}
|
||||
|
||||
export const service = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
||||
options: service.Options<A, I, R, MA, ME, MR, MP>
|
||||
): Effect.Effect<
|
||||
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
||||
never,
|
||||
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
||||
> => Effect.tap(
|
||||
make(options),
|
||||
form => Effect.forkScoped(run(form)),
|
||||
)
|
||||
|
||||
export const field = <A, I, R, SA, SE, SR, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||
self: Form<A, I, R, SA, SE, SR>,
|
||||
path: P,
|
||||
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => new FormFieldImpl(
|
||||
Subscribable.mapEffect(self.valueRef, Option.match({
|
||||
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
||||
onNone: () => Option.some(Option.none()),
|
||||
})),
|
||||
SubscriptionSubRef.makeFromPath(self.encodedValueRef, path),
|
||||
Subscribable.mapEffect(self.errorRef, Option.match({
|
||||
onSome: flow(
|
||||
ParseResult.ArrayFormatter.formatError,
|
||||
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
|
||||
),
|
||||
onNone: () => Effect.succeed([]),
|
||||
})),
|
||||
Subscribable.map(self.validationFiberRef, Option.isSome),
|
||||
Subscribable.map(self.submitStateRef, AsyncData.isLoading)
|
||||
)
|
||||
|
||||
|
||||
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 interface FormField<in out A, in out I = A>
|
||||
extends Pipeable.Pipeable {
|
||||
readonly [FormFieldTypeId]: FormFieldTypeId
|
||||
|
||||
readonly valueSubscribable: Subscribable.Subscribable<Option.Option<A>, NoSuchElementException>
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>
|
||||
readonly issuesSubscribable: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
||||
readonly isValidatingSubscribable: Subscribable.Subscribable<boolean>
|
||||
readonly isSubmittingSubscribable: Subscribable.Subscribable<boolean>
|
||||
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
|
||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
||||
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
||||
readonly isValidating: Subscribable.Subscribable<boolean>
|
||||
readonly isSubmitting: Subscribable.Subscribable<boolean>
|
||||
}
|
||||
|
||||
class FormFieldImpl<in out A, in out I = A>
|
||||
@@ -221,35 +238,57 @@ extends Pipeable.Class() implements FormField<A, I> {
|
||||
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
|
||||
|
||||
constructor(
|
||||
readonly valueSubscribable: Subscribable.Subscribable<Option.Option<A>, NoSuchElementException>,
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
||||
readonly issuesSubscribable: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
||||
readonly isValidatingSubscribable: Subscribable.Subscribable<boolean>,
|
||||
readonly isSubmittingSubscribable: Subscribable.Subscribable<boolean>,
|
||||
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
|
||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
||||
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
||||
readonly isValidating: Subscribable.Subscribable<boolean>,
|
||||
readonly isSubmitting: Subscribable.Subscribable<boolean>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
const FormFieldKeyTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormFieldKey")
|
||||
type FormFieldKeyTypeId = typeof FormFieldKeyTypeId
|
||||
|
||||
class FormFieldKey implements Equal.Equal {
|
||||
readonly [FormFieldKeyTypeId]: FormFieldKeyTypeId = FormFieldKeyTypeId
|
||||
constructor(readonly path: PropertyPath.PropertyPath) {}
|
||||
|
||||
[Equal.symbol](that: Equal.Equal) {
|
||||
return isFormFieldKey(that) && PropertyPath.equivalence(this.path, that.path)
|
||||
}
|
||||
[Hash.symbol]() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
export const isFormField = (u: unknown): u is FormField<unknown, unknown> => Predicate.hasProperty(u, FormFieldTypeId)
|
||||
const isFormFieldKey = (u: unknown): u is FormFieldKey => Predicate.hasProperty(u, FormFieldKeyTypeId)
|
||||
|
||||
|
||||
export const useSubmit = <A, I, R, SA, SE, SR>(
|
||||
self: Form<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<
|
||||
() => Promise<Option.Option<AsyncData.AsyncData<SA, SE>>>,
|
||||
never,
|
||||
SR
|
||||
> => Component.useCallbackPromise(() => submit(self), [self])
|
||||
|
||||
export const useField = <A, I, R, SA, SE, SR, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||
self: Form<A, I, R, SA, SE, SR>,
|
||||
export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||
self: Form<A, I, R, MA, ME, MR, MP>,
|
||||
path: P,
|
||||
): FormField<
|
||||
PropertyPath.ValueFromPath<A, P>,
|
||||
PropertyPath.ValueFromPath<I, P>
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: individual path components need to be compared
|
||||
> => React.useMemo(() => field(self, path), [self, ...path])
|
||||
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => {
|
||||
const _self = self as FormImpl<A, I, R, MA, ME, MR, MP>
|
||||
return new FormFieldImpl(
|
||||
Subscribable.mapEffect(_self.value, Option.match({
|
||||
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
||||
onNone: () => Option.some(Option.none()),
|
||||
})),
|
||||
SubscriptionSubRef.makeFromPath(_self.encodedValue, path),
|
||||
Subscribable.mapEffect(_self.error, Option.match({
|
||||
onSome: flow(
|
||||
ParseResult.ArrayFormatter.formatError,
|
||||
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
|
||||
),
|
||||
onNone: () => Effect.succeed([]),
|
||||
})),
|
||||
Subscribable.map(_self.validationFiber, Option.isSome),
|
||||
Subscribable.map(_self.mutation.result, result => Result.isRunning(result) || Result.isRefreshing(result)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export namespace useInput {
|
||||
export interface Options {
|
||||
@@ -262,20 +301,15 @@ export namespace useInput {
|
||||
}
|
||||
}
|
||||
|
||||
export const useInput: {
|
||||
<A, I>(
|
||||
field: FormField<A, I>,
|
||||
options?: useInput.Options,
|
||||
): Effect.Effect<useInput.Result<I>, NoSuchElementException, Scope.Scope>
|
||||
} = Effect.fnUntraced(function* <A, I>(
|
||||
export const useInput = Effect.fnUntraced(function* <A, I>(
|
||||
field: FormField<A, I>,
|
||||
options?: useInput.Options,
|
||||
) {
|
||||
): Effect.fn.Return<useInput.Result<I>, Cause.NoSuchElementException, Scope.Scope> {
|
||||
const internalValueRef = yield* Component.useOnChange(() => Effect.tap(
|
||||
Effect.andThen(field.encodedValueRef, SubscriptionRef.make),
|
||||
Effect.andThen(field.encodedValue, SubscriptionRef.make),
|
||||
internalValueRef => Effect.forkScoped(Effect.all([
|
||||
Stream.runForEach(
|
||||
Stream.drop(field.encodedValueRef, 1),
|
||||
Stream.drop(field.encodedValue, 1),
|
||||
upstreamEncodedValue => Effect.whenEffect(
|
||||
Ref.set(internalValueRef, upstreamEncodedValue),
|
||||
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
||||
@@ -288,7 +322,7 @@ export const useInput: {
|
||||
Stream.changesWith(Equal.equivalence()),
|
||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||
),
|
||||
internalValue => Ref.set(field.encodedValueRef, internalValue),
|
||||
internalValue => Ref.set(field.encodedValue, internalValue),
|
||||
),
|
||||
], { concurrency: "unbounded" })),
|
||||
), [field, options?.debounce])
|
||||
@@ -308,18 +342,13 @@ export namespace useOptionalInput {
|
||||
}
|
||||
}
|
||||
|
||||
export const useOptionalInput: {
|
||||
<A, I>(
|
||||
field: FormField<A, Option.Option<I>>,
|
||||
options: useOptionalInput.Options<I>,
|
||||
): Effect.Effect<useOptionalInput.Result<I>, NoSuchElementException, Scope.Scope>
|
||||
} = Effect.fnUntraced(function* <A, I>(
|
||||
export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
||||
field: FormField<A, Option.Option<I>>,
|
||||
options: useOptionalInput.Options<I>,
|
||||
) {
|
||||
): Effect.fn.Return<useOptionalInput.Result<I>, Cause.NoSuchElementException, Scope.Scope> {
|
||||
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap(
|
||||
Effect.andThen(
|
||||
field.encodedValueRef,
|
||||
field.encodedValue,
|
||||
Option.match({
|
||||
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
|
||||
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
|
||||
@@ -328,7 +357,7 @@ export const useOptionalInput: {
|
||||
|
||||
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([
|
||||
Stream.runForEach(
|
||||
Stream.drop(field.encodedValueRef, 1),
|
||||
Stream.drop(field.encodedValue, 1),
|
||||
|
||||
upstreamEncodedValue => Effect.whenEffect(
|
||||
Option.match(upstreamEncodedValue, {
|
||||
@@ -356,7 +385,7 @@ export const useOptionalInput: {
|
||||
Stream.changesWith(Equal.equivalence()),
|
||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||
),
|
||||
([enabled, internalValue]) => Ref.set(field.encodedValueRef, enabled ? Option.some(internalValue) : Option.none()),
|
||||
([enabled, internalValue]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||
),
|
||||
], { concurrency: "unbounded" })),
|
||||
), [field, options.debounce])
|
||||
|
||||
123
packages/effect-fc/src/Mutation.ts
Normal file
123
packages/effect-fc/src/Mutation.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { type Context, Effect, Equal, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
|
||||
import * as Result from "./Result.js"
|
||||
|
||||
|
||||
export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation")
|
||||
export type MutationTypeId = typeof MutationTypeId
|
||||
|
||||
export interface Mutation<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
|
||||
extends Pipeable.Pipeable {
|
||||
readonly [MutationTypeId]: MutationTypeId
|
||||
|
||||
readonly context: Context.Context<Scope.Scope | R>
|
||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||
readonly initialProgress: P
|
||||
|
||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||
|
||||
mutate(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||
}
|
||||
|
||||
export class MutationImpl<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
|
||||
extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
|
||||
readonly [MutationTypeId]: MutationTypeId = MutationTypeId
|
||||
|
||||
constructor(
|
||||
readonly context: Context.Context<Scope.Scope | NoInfer<R>>,
|
||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||
readonly initialProgress: P,
|
||||
|
||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
mutate(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
||||
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
||||
Effect.andThen(sub => this.watch(sub)),
|
||||
)
|
||||
}
|
||||
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
||||
return Effect.andThen(
|
||||
SubscriptionRef.set(this.latestKey, Option.some(key)),
|
||||
Effect.provide(this.start(key), this.context)
|
||||
)
|
||||
}
|
||||
|
||||
start(key: K): Effect.Effect<
|
||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||
never,
|
||||
Scope.Scope | R
|
||||
> {
|
||||
return this.result.pipe(
|
||||
Effect.map(previous => Result.isFinal(previous)
|
||||
? previous
|
||||
: undefined
|
||||
),
|
||||
Effect.andThen(previous => Result.unsafeForkEffect(
|
||||
Effect.onExit(this.f(key), () => Effect.andThen(
|
||||
Effect.all([Effect.fiberId, this.fiber]),
|
||||
([currentFiberId, fiber]) => Option.match(fiber, {
|
||||
onSome: v => Equal.equals(currentFiberId, v.id())
|
||||
? SubscriptionRef.set(this.fiber, Option.none())
|
||||
: Effect.void,
|
||||
onNone: () => Effect.void,
|
||||
})
|
||||
)),
|
||||
|
||||
{
|
||||
initialProgress: this.initialProgress,
|
||||
previous,
|
||||
} as Result.unsafeForkEffect.Options<A, E, P>,
|
||||
)),
|
||||
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
||||
Effect.map(([sub]) => sub),
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||
): Effect.Effect<Result.Final<A, E, P>> {
|
||||
return Effect.andThen(
|
||||
sub.get,
|
||||
initial => Stream.runFoldEffect(
|
||||
Stream.filter(sub.changes, Predicate.not(Result.isInitial)),
|
||||
initial,
|
||||
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
||||
),
|
||||
) as Effect.Effect<Result.Final<A, E, P>>
|
||||
}
|
||||
}
|
||||
|
||||
export const isMutation = (u: unknown): u is Mutation<unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, MutationTypeId)
|
||||
|
||||
export declare namespace make {
|
||||
export interface Options<K extends readonly any[] = never, A = void, E = never, R = never, P = never> {
|
||||
readonly f: (key: K) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
||||
readonly initialProgress?: P
|
||||
}
|
||||
}
|
||||
|
||||
export const make = Effect.fnUntraced(function* <const K extends readonly any[] = never, A = void, E = never, R = never, P = never>(
|
||||
options: make.Options<K, A, E, R, P>
|
||||
): Effect.fn.Return<
|
||||
Mutation<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||
never,
|
||||
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
|
||||
> {
|
||||
return new MutationImpl(
|
||||
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||
options.f as any,
|
||||
options.initialProgress as P,
|
||||
|
||||
yield* SubscriptionRef.make(Option.none<K>()),
|
||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
||||
)
|
||||
})
|
||||
14
packages/effect-fc/src/PubSub.ts
Normal file
14
packages/effect-fc/src/PubSub.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Effect, PubSub, type Scope } from "effect"
|
||||
import type * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
|
||||
|
||||
export const usePubSubFromReactiveValues = Effect.fnUntraced(function* <const A extends React.DependencyList>(
|
||||
values: A
|
||||
): Effect.fn.Return<PubSub.PubSub<A>, never, Scope.Scope> {
|
||||
const pubsub = yield* Component.useOnMount(() => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown))
|
||||
yield* Component.useReactEffect(() => Effect.unlessEffect(PubSub.publish(pubsub, values), PubSub.isShutdown(pubsub)), values)
|
||||
return pubsub
|
||||
})
|
||||
|
||||
export * from "effect/PubSub"
|
||||
192
packages/effect-fc/src/Query.ts
Normal file
192
packages/effect-fc/src/Query.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { type Cause, type Context, Effect, Fiber, identity, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
|
||||
import * as Result from "./Result.js"
|
||||
|
||||
|
||||
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
||||
export type QueryTypeId = typeof QueryTypeId
|
||||
|
||||
export interface Query<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
|
||||
extends Pipeable.Pipeable {
|
||||
readonly [QueryTypeId]: QueryTypeId
|
||||
|
||||
readonly context: Context.Context<Scope.Scope | R>
|
||||
readonly key: Stream.Stream<K>
|
||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||
readonly initialProgress: P
|
||||
|
||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||
|
||||
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||
readonly refetch: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
||||
readonly refetchSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
||||
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
||||
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
||||
}
|
||||
|
||||
export class QueryImpl<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
|
||||
extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
||||
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
||||
|
||||
constructor(
|
||||
readonly context: Context.Context<Scope.Scope | NoInfer<R>>,
|
||||
readonly key: Stream.Stream<K>,
|
||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||
readonly initialProgress: P,
|
||||
|
||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||
|
||||
readonly runSemaphore: Effect.Semaphore,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get interrupt(): Effect.Effect<void, never, never> {
|
||||
return Effect.andThen(this.fiber, Option.match({
|
||||
onSome: Fiber.interrupt,
|
||||
onNone: () => Effect.void,
|
||||
}))
|
||||
}
|
||||
|
||||
fetch(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||
return this.interrupt.pipe(
|
||||
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
||||
Effect.andThen(sub => this.watch(sub)),
|
||||
)
|
||||
}
|
||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
||||
return this.interrupt.pipe(
|
||||
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
||||
)
|
||||
}
|
||||
get refetch(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
||||
return this.interrupt.pipe(
|
||||
Effect.andThen(this.latestKey),
|
||||
Effect.andThen(identity),
|
||||
Effect.andThen(key => Effect.provide(this.start(key), this.context)),
|
||||
Effect.andThen(sub => this.watch(sub)),
|
||||
)
|
||||
}
|
||||
get refetchSubscribable(): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException> {
|
||||
return this.interrupt.pipe(
|
||||
Effect.andThen(this.latestKey),
|
||||
Effect.andThen(identity),
|
||||
Effect.andThen(key => Effect.provide(this.start(key), this.context)),
|
||||
)
|
||||
}
|
||||
get refresh(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
||||
return this.interrupt.pipe(
|
||||
Effect.andThen(this.latestKey),
|
||||
Effect.andThen(identity),
|
||||
Effect.andThen(key => Effect.provide(this.start(key, true), this.context)),
|
||||
Effect.andThen(sub => this.watch(sub)),
|
||||
)
|
||||
}
|
||||
get refreshSubscribable(): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException> {
|
||||
return this.interrupt.pipe(
|
||||
Effect.andThen(this.latestKey),
|
||||
Effect.andThen(identity),
|
||||
Effect.andThen(key => Effect.provide(this.start(key, true), this.context)),
|
||||
)
|
||||
}
|
||||
|
||||
start(
|
||||
key: K,
|
||||
refresh?: boolean,
|
||||
): Effect.Effect<
|
||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||
never,
|
||||
Scope.Scope | R
|
||||
> {
|
||||
return this.result.pipe(
|
||||
Effect.map(previous => Result.isFinal(previous)
|
||||
? previous
|
||||
: undefined
|
||||
),
|
||||
Effect.andThen(previous => Result.unsafeForkEffect(
|
||||
Effect.onExit(this.f(key), () => SubscriptionRef.set(this.fiber, Option.none())),
|
||||
{
|
||||
initialProgress: this.initialProgress,
|
||||
refresh: refresh && previous,
|
||||
previous,
|
||||
} as Result.unsafeForkEffect.Options<A, E, P>,
|
||||
)),
|
||||
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
||||
Effect.map(([sub]) => sub),
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||
): Effect.Effect<Result.Final<A, E, P>> {
|
||||
return Effect.andThen(
|
||||
sub.get,
|
||||
initial => Stream.runFoldEffect(
|
||||
Stream.filter(sub.changes, Predicate.not(Result.isInitial)),
|
||||
initial,
|
||||
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
||||
),
|
||||
) as Effect.Effect<Result.Final<A, E, P>>
|
||||
}
|
||||
}
|
||||
|
||||
export const isQuery = (u: unknown): u is Query<unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
|
||||
|
||||
export declare namespace make {
|
||||
export interface Options<K extends readonly any[], A, E = never, R = never, P = never> {
|
||||
readonly key: Stream.Stream<K>
|
||||
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
||||
readonly initialProgress?: P
|
||||
}
|
||||
}
|
||||
|
||||
export const make = Effect.fnUntraced(function* <K extends readonly any[], A, E = never, R = never, P = never>(
|
||||
options: make.Options<K, A, E, R, P>
|
||||
): Effect.fn.Return<
|
||||
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||
never,
|
||||
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
|
||||
> {
|
||||
return new QueryImpl(
|
||||
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||
options.key,
|
||||
options.f as any,
|
||||
options.initialProgress as P,
|
||||
|
||||
yield* SubscriptionRef.make(Option.none<K>()),
|
||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
||||
|
||||
yield* Effect.makeSemaphore(1),
|
||||
)
|
||||
})
|
||||
|
||||
export const service = <K extends readonly any[], A, E = never, R = never, P = never>(
|
||||
options: make.Options<K, A, E, R, P>
|
||||
): Effect.Effect<
|
||||
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||
never,
|
||||
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
|
||||
> => Effect.tap(
|
||||
make(options),
|
||||
query => Effect.forkScoped(run(query)),
|
||||
)
|
||||
|
||||
export const run = <K extends readonly any[], A, E, R, P>(
|
||||
self: Query<K, A, E, R, P>
|
||||
): Effect.Effect<void> => {
|
||||
const _self = self as QueryImpl<K, A, E, R, P>
|
||||
return Stream.runForEach(_self.key, key => _self.interrupt.pipe(
|
||||
Effect.andThen(SubscriptionRef.set(_self.latestKey, Option.some(key))),
|
||||
Effect.andThen(_self.start(key)),
|
||||
Effect.andThen(sub => Effect.forkScoped(_self.watch(sub))),
|
||||
Effect.provide(_self.context),
|
||||
_self.runSemaphore.withPermits(1),
|
||||
))
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
|
||||
import { Effect, Layer, ManagedRuntime, Predicate, type Runtime } from "effect"
|
||||
import { Effect, Layer, ManagedRuntime, Predicate, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
import * as ErrorObserver from "./ErrorObserver.js"
|
||||
|
||||
|
||||
export const TypeId: unique symbol = Symbol.for("@effect-fc/ReactRuntime/ReactRuntime")
|
||||
@@ -16,16 +17,21 @@ export interface ReactRuntime<R, ER> {
|
||||
|
||||
const ReactRuntimeProto = Object.freeze({ [TypeId]: TypeId } as const)
|
||||
|
||||
export const Prelude: Layer.Layer<Component.ScopeMap | ErrorObserver.ErrorObserver> = Layer.mergeAll(
|
||||
Component.ScopeMap.Default,
|
||||
ErrorObserver.layer,
|
||||
)
|
||||
|
||||
|
||||
export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown> => Predicate.hasProperty(u, TypeId)
|
||||
|
||||
export const make = <R, ER>(
|
||||
layer: Layer.Layer<R, ER>,
|
||||
memoMap?: Layer.MemoMap,
|
||||
): ReactRuntime<R | Component.ScopeMap, ER> => Object.setPrototypeOf(
|
||||
): ReactRuntime<Layer.Layer.Success<typeof Prelude> | R, ER> => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, {
|
||||
runtime: ManagedRuntime.make(
|
||||
Layer.merge(layer, Component.ScopeMap.Default),
|
||||
Layer.merge(layer, Prelude),
|
||||
memoMap,
|
||||
),
|
||||
// biome-ignore lint/style/noNonNullAssertion: context initialization
|
||||
@@ -54,16 +60,20 @@ export const Provider = <R, ER>(
|
||||
)
|
||||
}
|
||||
|
||||
interface ProviderInnerProps<R, ER> {
|
||||
readonly runtime: ReactRuntime<R, ER>
|
||||
readonly promise: Promise<Runtime.Runtime<R>>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
|
||||
const ProviderInner = <R, ER>(
|
||||
{ runtime, promise, children }: ProviderInnerProps<R, ER>
|
||||
): React.ReactNode => React.createElement(
|
||||
runtime.context,
|
||||
{ value: React.use(promise) },
|
||||
children,
|
||||
)
|
||||
{ runtime, promise, children }: {
|
||||
readonly runtime: ReactRuntime<R, ER>
|
||||
readonly promise: Promise<Runtime.Runtime<R>>
|
||||
readonly children?: React.ReactNode
|
||||
}
|
||||
): React.ReactNode => {
|
||||
const effectRuntime = React.use(promise)
|
||||
const scope = Runtime.runSync(effectRuntime)(Component.useScope([effectRuntime]))
|
||||
Runtime.runSync(effectRuntime)(Effect.provideService(
|
||||
Component.useOnChange(() => Effect.addFinalizer(() => runtime.runtime.disposeEffect), [scope]),
|
||||
Scope.Scope,
|
||||
scope,
|
||||
))
|
||||
|
||||
return React.createElement(runtime.context, { value: effectRuntime }, children)
|
||||
}
|
||||
|
||||
276
packages/effect-fc/src/Result.ts
Normal file
276
packages/effect-fc/src/Result.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Option, Pipeable, Predicate, PubSub, pipe, Ref, type Scope, Stream, Subscribable } from "effect"
|
||||
|
||||
|
||||
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
||||
export type ResultTypeId = typeof ResultTypeId
|
||||
|
||||
export type Result<A, E = never, P = never> = (
|
||||
| Initial
|
||||
| Running<P>
|
||||
| Final<A, E, P>
|
||||
)
|
||||
|
||||
export type Final<A, E = never, P = never> = (
|
||||
| Success<A>
|
||||
| (Success<A> & Refreshing<P>)
|
||||
| Failure<A, E>
|
||||
| (Failure<A, E> & Refreshing<P>)
|
||||
)
|
||||
|
||||
export namespace Result {
|
||||
export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
|
||||
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 Failure<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer E, infer _P>] ? E : never
|
||||
export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never
|
||||
}
|
||||
|
||||
export interface Initial extends Result.Prototype {
|
||||
readonly _tag: "Initial"
|
||||
}
|
||||
|
||||
export interface Running<P = never> extends Result.Prototype {
|
||||
readonly _tag: "Running"
|
||||
readonly progress: P
|
||||
}
|
||||
|
||||
export interface Success<A> extends Result.Prototype {
|
||||
readonly _tag: "Success"
|
||||
readonly value: A
|
||||
}
|
||||
|
||||
export interface Failure<A, E = never> extends Result.Prototype {
|
||||
readonly _tag: "Failure"
|
||||
readonly cause: Cause.Cause<E>
|
||||
readonly previousSuccess: Option.Option<Success<A>>
|
||||
}
|
||||
|
||||
export interface Refreshing<P = never> {
|
||||
readonly refreshing: true
|
||||
readonly progress: P
|
||||
}
|
||||
|
||||
|
||||
const ResultPrototype = Object.freeze({
|
||||
...Pipeable.Prototype,
|
||||
[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)
|
||||
|
||||
|
||||
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
|
||||
export const isFinal = (u: unknown): u is Final<unknown, unknown, unknown> => isResult(u) && (isSuccess(u) || isFailure(u))
|
||||
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
|
||||
export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u._tag === "Running"
|
||||
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 isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing") && u.refreshing
|
||||
|
||||
export const initial: {
|
||||
(): Initial
|
||||
<A, E = never, P = never>(): Result<A, E, P>
|
||||
} = (): Initial => Object.setPrototypeOf({ _tag: "Initial" }, ResultPrototype)
|
||||
export const running = <P = never>(progress?: P): Running<P> => Object.setPrototypeOf({ _tag: "Running", progress }, ResultPrototype)
|
||||
export const succeed = <A>(value: A): Success<A> => Object.setPrototypeOf({ _tag: "Success", value }, ResultPrototype)
|
||||
|
||||
export const fail = <E, A = never>(
|
||||
cause: Cause.Cause<E>,
|
||||
previousSuccess?: Success<NoInfer<A>>,
|
||||
): Failure<A, E> => Object.setPrototypeOf({
|
||||
_tag: "Failure",
|
||||
cause,
|
||||
previousSuccess: Option.fromNullable(previousSuccess),
|
||||
}, ResultPrototype)
|
||||
|
||||
export const refreshing = <R extends Success<any> | Failure<any, any>, P = never>(
|
||||
result: R,
|
||||
progress?: P,
|
||||
): Omit<R, keyof Refreshing<Result.Progress<R>>> & Refreshing<P> => Object.setPrototypeOf(
|
||||
Object.assign({}, result, { refreshing: true, progress }),
|
||||
Object.getPrototypeOf(result),
|
||||
)
|
||||
|
||||
export const fromExit = <A, E>(
|
||||
exit: Exit.Exit<A, E>,
|
||||
previousSuccess?: Success<NoInfer<A>>,
|
||||
): Success<A> | Failure<A, E> => exit._tag === "Success"
|
||||
? succeed(exit.value)
|
||||
: fail(exit.cause, previousSuccess)
|
||||
|
||||
export const toExit = <A, E, P>(
|
||||
self: Result<A, E, P>
|
||||
): Exit.Exit<A, E | Cause.NoSuchElementException> => {
|
||||
switch (self._tag) {
|
||||
case "Success":
|
||||
return Exit.succeed(self.value)
|
||||
case "Failure":
|
||||
return Exit.failCause(self.cause)
|
||||
default:
|
||||
return Exit.fail(new Cause.NoSuchElementException())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface State<A, E = never, P = never> {
|
||||
readonly get: Effect.Effect<Result<A, E, P>>
|
||||
readonly set: (v: Result<A, E, P>) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export const State = <A, E = never, P = never>(): Context.Tag<State<A, E, P>, State<A, E, P>> => Context.GenericTag("@effect-fc/Result/State")
|
||||
|
||||
export interface Progress<P = never> {
|
||||
readonly update: <E, R>(
|
||||
f: (previous: P) => Effect.Effect<P, E, R>
|
||||
) => Effect.Effect<void, PreviousResultNotRunningNorRefreshing | E, R>
|
||||
}
|
||||
|
||||
export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{
|
||||
readonly previous: Result<unknown, unknown, unknown>
|
||||
}> {}
|
||||
|
||||
export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress")
|
||||
|
||||
export const makeProgressLayer = <A, E, P = never>(): Layer.Layer<
|
||||
Progress<P>,
|
||||
never,
|
||||
State<A, E, P>
|
||||
> => Layer.effect(Progress<P>(), Effect.gen(function*() {
|
||||
const state = yield* State<A, E, P>()
|
||||
|
||||
return {
|
||||
update: <E, R>(f: (previous: P) => Effect.Effect<P, E, R>) => Effect.Do.pipe(
|
||||
Effect.bind("previous", () => Effect.andThen(state.get, previous =>
|
||||
isRunning(previous) || isRefreshing(previous)
|
||||
? Effect.succeed(previous)
|
||||
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })),
|
||||
)),
|
||||
Effect.bind("progress", ({ previous }) => f(previous.progress)),
|
||||
Effect.let("next", ({ previous, progress }) => Object.setPrototypeOf(
|
||||
Object.assign({}, previous, { progress }),
|
||||
Object.getPrototypeOf(previous),
|
||||
)),
|
||||
Effect.andThen(({ next }) => state.set(next)),
|
||||
),
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
export namespace unsafeForkEffect {
|
||||
export type OutputContext<A, E, R, P> = Exclude<R, State<A, E, P> | Progress<P> | Progress<never>>
|
||||
|
||||
export type Options<A, E, P> = {
|
||||
readonly initialProgress?: P
|
||||
readonly previous?: Final<A, E, P>
|
||||
} & (
|
||||
| {
|
||||
readonly refresh: true
|
||||
readonly previous: Final<A, E, P>
|
||||
}
|
||||
| {
|
||||
readonly refresh?: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const unsafeForkEffect = <A, E, R, P = never>(
|
||||
effect: Effect.Effect<A, E, R>,
|
||||
options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||
): Effect.Effect<
|
||||
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||
never,
|
||||
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
|
||||
> => Effect.Do.pipe(
|
||||
Effect.bind("ref", () => Ref.make(initial<A, E, P>())),
|
||||
Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()),
|
||||
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
|
||||
Effect.andThen(state => state.set(options?.refresh
|
||||
? refreshing(options.previous, options?.initialProgress) as Result<A, E, P>
|
||||
: running(options?.initialProgress)
|
||||
).pipe(
|
||||
Effect.andThen(effect),
|
||||
Effect.onExit(exit => Effect.andThen(
|
||||
state.set(fromExit(exit, (options?.previous && isSuccess(options.previous)) ? options.previous : undefined)),
|
||||
Effect.forkScoped(PubSub.shutdown(pubsub)),
|
||||
)),
|
||||
)),
|
||||
Effect.provide(Layer.empty.pipe(
|
||||
Layer.provideMerge(makeProgressLayer<A, E, P>()),
|
||||
Layer.provideMerge(Layer.succeed(State<A, E, P>(), {
|
||||
get: ref,
|
||||
set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v))
|
||||
})),
|
||||
)),
|
||||
))),
|
||||
Effect.map(({ ref, pubsub, fiber }) => [
|
||||
Subscribable.make({
|
||||
get: ref,
|
||||
changes: Stream.unwrapScoped(Effect.map(
|
||||
Effect.all([ref, Stream.fromPubSub(pubsub, { scoped: true })]),
|
||||
([latest, stream]) => Stream.concat(Stream.make(latest), stream),
|
||||
)),
|
||||
}),
|
||||
fiber,
|
||||
]),
|
||||
) as Effect.Effect<
|
||||
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||
never,
|
||||
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
|
||||
>
|
||||
|
||||
export namespace forkEffect {
|
||||
export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R
|
||||
export type OutputContext<A, E, R, P> = unsafeForkEffect.OutputContext<A, E, R, P>
|
||||
export type Options<A, E, P> = unsafeForkEffect.Options<A, E, P>
|
||||
}
|
||||
|
||||
export const forkEffect: {
|
||||
<A, E, R, P = never>(
|
||||
effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>,
|
||||
options?: forkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||
): Effect.Effect<
|
||||
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||
never,
|
||||
Scope.Scope | forkEffect.OutputContext<A, E, R, P>
|
||||
>
|
||||
} = unsafeForkEffect
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Equivalence, Option, PubSub, Ref, type Scope, Stream } from "effect"
|
||||
import { Effect, Equivalence, Option, Stream } from "effect"
|
||||
import * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
|
||||
@@ -30,29 +30,4 @@ export const useStream: {
|
||||
return reactStateValue as Option.Some<A>
|
||||
})
|
||||
|
||||
export const useStreamFromReactiveValues: {
|
||||
<const A extends React.DependencyList>(
|
||||
values: A
|
||||
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
||||
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
|
||||
const { latest, pubsub, stream } = yield* Component.useOnMount(() => Effect.Do.pipe(
|
||||
Effect.bind("latest", () => Ref.make(values)),
|
||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
||||
Effect.flatMap(a => Effect.map(
|
||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||
s => Stream.concat(Stream.make(a), s),
|
||||
)),
|
||||
Stream.unwrapScoped,
|
||||
)),
|
||||
))
|
||||
|
||||
yield* Component.useReactEffect(() => Ref.set(latest, values).pipe(
|
||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
||||
), values)
|
||||
|
||||
return stream
|
||||
})
|
||||
|
||||
export * from "effect/Stream"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Equivalence, pipe, type Scope, Stream, Subscribable } from "effect"
|
||||
import { Effect, Equivalence, Stream, Subscribable } from "effect"
|
||||
import * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
|
||||
@@ -16,30 +16,35 @@ export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<
|
||||
changes: Stream.zipLatestAll(...elements.map(v => v.changes)),
|
||||
}) as any
|
||||
|
||||
export const useSubscribables: {
|
||||
<const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||
...elements: T
|
||||
): Effect.Effect<
|
||||
[T[number]] extends [never]
|
||||
? never
|
||||
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never },
|
||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
||||
([T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never) | Scope.Scope
|
||||
>
|
||||
} = Effect.fnUntraced(function* <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||
...elements: T
|
||||
) {
|
||||
export declare namespace useSubscribables {
|
||||
export type Success<T extends readonly Subscribable.Subscribable<any, any, any>[]> = [T[number]] extends [never]
|
||||
? never
|
||||
: { [K in keyof T]: T[K] extends Subscribable.Subscribable<infer A, infer _E, infer _R> ? A : never }
|
||||
|
||||
export interface Options<A> {
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}
|
||||
}
|
||||
|
||||
export const useSubscribables = Effect.fnUntraced(function* <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||
elements: T,
|
||||
options?: useSubscribables.Options<useSubscribables.Success<NoInfer<T>>>,
|
||||
): Effect.fn.Return<
|
||||
useSubscribables.Success<T>,
|
||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer E, infer _R> ? E : never,
|
||||
[T[number]] extends [never] ? never : T[number] extends Subscribable.Subscribable<infer _A, infer _E, infer R> ? R : never
|
||||
> {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(
|
||||
yield* Component.useOnMount(() => Effect.all(elements.map(v => v.get)))
|
||||
)
|
||||
|
||||
yield* Component.useReactEffect(() => Effect.forkScoped(pipe(
|
||||
elements.map(ref => Stream.changesWith(ref.changes, Equivalence.strict())),
|
||||
streams => Stream.zipLatestAll(...streams),
|
||||
yield* Component.useReactEffect(() => Stream.zipLatestAll(...elements.map(ref => ref.changes)).pipe(
|
||||
Stream.changesWith((options?.equivalence as Equivalence.Equivalence<any[]> | undefined) ?? Equivalence.array(Equivalence.strict())),
|
||||
Stream.runForEach(v =>
|
||||
Effect.sync(() => setReactStateValue(v))
|
||||
),
|
||||
)), elements)
|
||||
Effect.forkScoped,
|
||||
), elements)
|
||||
|
||||
return reactStateValue as any
|
||||
})
|
||||
|
||||
@@ -1,41 +1,54 @@
|
||||
import { Effect, Equivalence, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
||||
import { Effect, Equivalence, Ref, Stream, SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import * as Component from "./Component.js"
|
||||
import * as SetStateAction from "./SetStateAction.js"
|
||||
|
||||
|
||||
export const useSubscriptionRefState: {
|
||||
<A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>
|
||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>], never, Scope.Scope>
|
||||
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||
export declare namespace useSubscriptionRefState {
|
||||
export interface Options<A> {
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}
|
||||
}
|
||||
|
||||
export const useSubscriptionRefState = Effect.fnUntraced(function* <A>(
|
||||
ref: SubscriptionRef.SubscriptionRef<A>,
|
||||
options?: useSubscriptionRefState.Options<NoInfer<A>>,
|
||||
): Effect.fn.Return<readonly [A, React.Dispatch<React.SetStateAction<A>>]> {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => ref))
|
||||
|
||||
yield* Component.useReactEffect(() => Effect.forkScoped(
|
||||
Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||
Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
|
||||
v => Effect.sync(() => setReactStateValue(v)),
|
||||
)
|
||||
), [ref])
|
||||
|
||||
const setValue = yield* Component.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||
Effect.andThen(
|
||||
const setValue = yield* Component.useCallbackSync(
|
||||
(setStateAction: React.SetStateAction<A>) => Effect.andThen(
|
||||
Ref.updateAndGet(ref, prevState => SetStateAction.value(setStateAction, prevState)),
|
||||
v => setReactStateValue(v),
|
||||
),
|
||||
[ref])
|
||||
[ref],
|
||||
)
|
||||
|
||||
return [reactStateValue, setValue]
|
||||
})
|
||||
|
||||
export const useSubscriptionRefFromState: {
|
||||
<A>(state: readonly [A, React.Dispatch<React.SetStateAction<A>>]): Effect.Effect<SubscriptionRef.SubscriptionRef<A>, never, Scope.Scope>
|
||||
} = Effect.fnUntraced(function*([value, setValue]) {
|
||||
export declare namespace useSubscriptionRefFromState {
|
||||
export interface Options<A> {
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}
|
||||
}
|
||||
|
||||
export const useSubscriptionRefFromState = Effect.fnUntraced(function* <A>(
|
||||
[value, setValue]: readonly [A, React.Dispatch<React.SetStateAction<A>>],
|
||||
options?: useSubscriptionRefFromState.Options<NoInfer<A>>,
|
||||
): Effect.fn.Return<SubscriptionRef.SubscriptionRef<A>> {
|
||||
const ref = yield* Component.useOnChange(() => Effect.tap(
|
||||
SubscriptionRef.make(value),
|
||||
ref => Effect.forkScoped(
|
||||
Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, Equivalence.strict()),
|
||||
Stream.changesWith(ref.changes, options?.equivalence ?? Equivalence.strict()),
|
||||
v => Effect.sync(() => setValue(v)),
|
||||
)
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Chunk, Effect, Effectable, Option, Predicate, Readable, Ref, Stream, Su
|
||||
import * as PropertyPath from "./PropertyPath.js"
|
||||
|
||||
|
||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("effect-fc/SubscriptionSubRef/SubscriptionSubRef")
|
||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("@effect-fc/SubscriptionSubRef/SubscriptionSubRef")
|
||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||
|
||||
export interface SubscriptionSubRef<in out A, in out B extends SubscriptionRef.SubscriptionRef<any>>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
export * as Async from "./Async.js"
|
||||
export * as Component from "./Component.js"
|
||||
export * as ErrorObserver from "./ErrorObserver.js"
|
||||
export * as Form from "./Form.js"
|
||||
export * as Memoized from "./Memoized.js"
|
||||
export * as Mutation from "./Mutation.js"
|
||||
export * as PropertyPath from "./PropertyPath.js"
|
||||
export * as PubSub from "./PubSub.js"
|
||||
export * as Query from "./Query.js"
|
||||
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 Subscribable from "./Subscribable.js"
|
||||
|
||||
@@ -13,32 +13,30 @@
|
||||
"clean:modules": "rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/react-router": "^1.132.31",
|
||||
"@tanstack/react-router-devtools": "^1.132.31",
|
||||
"@tanstack/router-plugin": "^1.132.31",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"globals": "^16.4.0",
|
||||
"@tanstack/react-router": "^1.139.12",
|
||||
"@tanstack/react-router-devtools": "^1.139.12",
|
||||
"@tanstack/router-plugin": "^1.139.12",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"globals": "^16.5.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"type-fest": "^5.0.1",
|
||||
"vite": "^7.1.8"
|
||||
"type-fest": "^5.2.0",
|
||||
"vite": "^7.2.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.92.1",
|
||||
"@effect/platform-browser": "^0.72.0",
|
||||
"@effect/platform": "^0.93.6",
|
||||
"@effect/platform-browser": "^0.73.0",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@typed/async-data": "^0.13.1",
|
||||
"@typed/id": "^0.17.2",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"effect": "^3.18.1",
|
||||
"effect": "^3.19.8",
|
||||
"effect-fc": "workspace:*",
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "^19.2.0",
|
||||
"effect": "^3.18.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"effect": "^3.19.8",
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInp
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported
|
||||
: { optional: false, ...yield* Form.useInput(props.field, props) }
|
||||
|
||||
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables(
|
||||
props.field.issuesSubscribable,
|
||||
props.field.isValidatingSubscribable,
|
||||
props.field.isSubmittingSubscribable,
|
||||
)
|
||||
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
|
||||
props.field.issues,
|
||||
props.field.isValidating,
|
||||
props.field.isSubmitting,
|
||||
])
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="1">
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as ResultRouteImport } from './routes/result'
|
||||
import { Route as QueryRouteImport } from './routes/query'
|
||||
import { Route as FormRouteImport } from './routes/form'
|
||||
import { Route as BlankRouteImport } from './routes/blank'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
@@ -16,6 +18,16 @@ import { Route as DevMemoRouteImport } from './routes/dev/memo'
|
||||
import { Route as DevContextRouteImport } from './routes/dev/context'
|
||||
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
||||
|
||||
const ResultRoute = ResultRouteImport.update({
|
||||
id: '/result',
|
||||
path: '/result',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const QueryRoute = QueryRouteImport.update({
|
||||
id: '/query',
|
||||
path: '/query',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const FormRoute = FormRouteImport.update({
|
||||
id: '/form',
|
||||
path: '/form',
|
||||
@@ -51,6 +63,8 @@ export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/query': typeof QueryRoute
|
||||
'/result': typeof ResultRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/context': typeof DevContextRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -59,6 +73,8 @@ export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/query': typeof QueryRoute
|
||||
'/result': typeof ResultRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/context': typeof DevContextRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -68,6 +84,8 @@ export interface FileRoutesById {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/query': typeof QueryRoute
|
||||
'/result': typeof ResultRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/context': typeof DevContextRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -78,6 +96,8 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/query'
|
||||
| '/result'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/context'
|
||||
| '/dev/memo'
|
||||
@@ -86,6 +106,8 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/query'
|
||||
| '/result'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/context'
|
||||
| '/dev/memo'
|
||||
@@ -94,6 +116,8 @@ export interface FileRouteTypes {
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/query'
|
||||
| '/result'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/context'
|
||||
| '/dev/memo'
|
||||
@@ -103,6 +127,8 @@ export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
BlankRoute: typeof BlankRoute
|
||||
FormRoute: typeof FormRoute
|
||||
QueryRoute: typeof QueryRoute
|
||||
ResultRoute: typeof ResultRoute
|
||||
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
||||
DevContextRoute: typeof DevContextRoute
|
||||
DevMemoRoute: typeof DevMemoRoute
|
||||
@@ -110,6 +136,20 @@ export interface RootRouteChildren {
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/result': {
|
||||
id: '/result'
|
||||
path: '/result'
|
||||
fullPath: '/result'
|
||||
preLoaderRoute: typeof ResultRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/query': {
|
||||
id: '/query'
|
||||
path: '/query'
|
||||
fullPath: '/query'
|
||||
preLoaderRoute: typeof QueryRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/form': {
|
||||
id: '/form'
|
||||
path: '/form'
|
||||
@@ -159,6 +199,8 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
BlankRoute: BlankRoute,
|
||||
FormRoute: FormRoute,
|
||||
QueryRoute: QueryRoute,
|
||||
ResultRoute: ResultRoute,
|
||||
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
||||
DevContextRoute: DevContextRoute,
|
||||
DevMemoRoute: DevMemoRoute,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button, Container, Flex } from "@radix-ui/themes"
|
||||
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Option, ParseResult, Schema } from "effect"
|
||||
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||
import { Component, Form, Subscribable } from "effect-fc"
|
||||
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
@@ -23,6 +23,21 @@ const RegisterFormSchema = Schema.Struct({
|
||||
birth: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
|
||||
})
|
||||
|
||||
const RegisterFormSubmitSchema = Schema.Struct({
|
||||
email: Schema.transformOrFail(
|
||||
Schema.String,
|
||||
Schema.String,
|
||||
{
|
||||
decode: (input, _options, ast) => input !== "admin@admin.com"
|
||||
? ParseResult.succeed(input)
|
||||
: ParseResult.fail(new ParseResult.Type(ast, input, "This email is already in use.")),
|
||||
encode: ParseResult.succeed,
|
||||
},
|
||||
),
|
||||
password: Schema.String,
|
||||
birth: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
|
||||
})
|
||||
|
||||
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
scoped: Form.service({
|
||||
schema: RegisterFormSchema.pipe(
|
||||
@@ -39,19 +54,22 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
),
|
||||
|
||||
initialEncodedValue: { email: "", password: "", birth: Option.none() },
|
||||
onSubmit: v => Effect.sleep("500 millis").pipe(
|
||||
Effect.andThen(Console.log(v)),
|
||||
Effect.andThen(Effect.sync(() => alert("Done!"))),
|
||||
),
|
||||
f: Effect.fnUntraced(function*([value]) {
|
||||
yield* Effect.sleep("500 millis")
|
||||
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
|
||||
}),
|
||||
debounce: "500 millis",
|
||||
})
|
||||
}) {}
|
||||
|
||||
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
|
||||
const form = yield* RegisterForm
|
||||
const submit = yield* Form.useSubmit(form)
|
||||
const [canSubmit] = yield* Subscribable.useSubscribables(form.canSubmitSubscribable)
|
||||
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
||||
form.canSubmit,
|
||||
form.mutation.result,
|
||||
])
|
||||
|
||||
const runPromise = yield* Component.useRunPromise()
|
||||
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||
|
||||
yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||
@@ -64,27 +82,35 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
|
||||
<Container width="300">
|
||||
<form onSubmit={e => {
|
||||
e.preventDefault()
|
||||
void submit()
|
||||
void runPromise(form.submit)
|
||||
}}>
|
||||
<Flex direction="column" gap="2">
|
||||
<TextFieldFormInputFC
|
||||
field={Form.useField(form, ["email"])}
|
||||
field={yield* form.field(["email"])}
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
field={Form.useField(form, ["password"])}
|
||||
field={yield* form.field(["password"])}
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
optional
|
||||
type="datetime-local"
|
||||
field={Form.useField(form, ["birth"])}
|
||||
field={yield* form.field(["birth"])}
|
||||
defaultValue=""
|
||||
/>
|
||||
|
||||
<Button disabled={!canSubmit}>Submit</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
|
||||
{Match.value(submitResult).pipe(
|
||||
Match.tag("Initial", () => <></>),
|
||||
Match.tag("Running", () => <Text>Submitting...</Text>),
|
||||
Match.tag("Success", () => <Text>Submitted successfully!</Text>),
|
||||
Match.tag("Failure", e => <Text>Error: {e.cause.toString()}</Text>),
|
||||
Match.exhaustive,
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}) {}
|
||||
|
||||
116
packages/example/src/routes/query.tsx
Normal file
116
packages/example/src/routes/query.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { HttpClient, type HttpClientError } from "@effect/platform"
|
||||
import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
||||
import { Component, ErrorObserver, Mutation, Query, Result, Subscribable, SubscriptionRef } from "effect-fc"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const Post = Schema.Struct({
|
||||
userId: Schema.Int,
|
||||
id: Schema.Int,
|
||||
title: Schema.String,
|
||||
body: Schema.String,
|
||||
})
|
||||
|
||||
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||
const runPromise = yield* Component.useRunPromise()
|
||||
|
||||
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||
const idRef = yield* SubscriptionRef.make(1)
|
||||
const key = Stream.zipLatest(Stream.make("posts" as const), idRef.changes)
|
||||
|
||||
const query = yield* Query.service({
|
||||
key,
|
||||
f: ([, id]) => HttpClient.HttpClient.pipe(
|
||||
Effect.tap(Effect.sleep("500 millis")),
|
||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
),
|
||||
})
|
||||
|
||||
const mutation = yield* Mutation.make({
|
||||
f: ([id]: readonly [id: number]) => HttpClient.HttpClient.pipe(
|
||||
Effect.tap(Effect.sleep("500 millis")),
|
||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
),
|
||||
})
|
||||
|
||||
return [idRef, query, mutation] as const
|
||||
}))
|
||||
|
||||
const [id, setId] = yield* SubscriptionRef.useSubscriptionRefState(idRef)
|
||||
const [queryResult, mutationResult] = yield* Subscribable.useSubscribables([query.result, mutation.result])
|
||||
|
||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||
Effect.andThen(observer => observer.subscribe),
|
||||
Effect.andThen(Stream.fromQueue),
|
||||
Stream.unwrapScoped,
|
||||
Stream.runForEach(flow(
|
||||
Cause.failures,
|
||||
Chunk.findFirst(e => e._tag === "RequestError" || e._tag === "ResponseError"),
|
||||
Option.match({
|
||||
onSome: e => Console.log("ResultView HttpClient error", e),
|
||||
onNone: () => Effect.void,
|
||||
}),
|
||||
)),
|
||||
Effect.forkScoped,
|
||||
))
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
<Slider
|
||||
value={[id]}
|
||||
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{Match.value(queryResult).pipe(
|
||||
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||
Match.tag("Success", result => <>
|
||||
<Heading>{result.value.title}</Heading>
|
||||
<Text>{result.value.body}</Text>
|
||||
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
|
||||
</>),
|
||||
Match.tag("Failure", result =>
|
||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||
),
|
||||
Match.orElse(() => <></>),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Flex direction="row" justify="center" align="center" gap="1">
|
||||
<Button onClick={() => runPromise(query.refresh)}>Refresh</Button>
|
||||
<Button onClick={() => runPromise(query.refetch)}>Refetch</Button>
|
||||
</Flex>
|
||||
|
||||
<div>
|
||||
{Match.value(mutationResult).pipe(
|
||||
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||
Match.tag("Success", result => <>
|
||||
<Heading>{result.value.title}</Heading>
|
||||
<Text>{result.value.body}</Text>
|
||||
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
|
||||
</>),
|
||||
Match.tag("Failure", result =>
|
||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||
),
|
||||
Match.orElse(() => <></>),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Flex direction="row" justify="center" align="center" gap="1">
|
||||
<Button onClick={() => runPromise(Effect.andThen(idRef, id => mutation.mutate([id])))}>Mutate</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
|
||||
export const Route = createFileRoute("/query")({
|
||||
component: Component.withRuntime(ResultView, runtime.context)
|
||||
})
|
||||
60
packages/example/src/routes/result.tsx
Normal file
60
packages/example/src/routes/result.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { HttpClient, type HttpClientError } from "@effect/platform"
|
||||
import { Container, Heading, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
||||
import { Component, ErrorObserver, Result, Subscribable } from "effect-fc"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const Post = Schema.Struct({
|
||||
userId: Schema.Int,
|
||||
id: Schema.Int,
|
||||
title: Schema.String,
|
||||
body: Schema.String,
|
||||
})
|
||||
|
||||
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||
const [resultSubscribable] = yield* Component.useOnMount(() => HttpClient.HttpClient.pipe(
|
||||
Effect.andThen(client => client.get("https://jsonplaceholder.typicode.com/posts/1")),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
Effect.tap(Effect.sleep("250 millis")),
|
||||
Result.forkEffect,
|
||||
))
|
||||
const [result] = yield* Subscribable.useSubscribables([resultSubscribable])
|
||||
|
||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||
Effect.andThen(observer => observer.subscribe),
|
||||
Effect.andThen(Stream.fromQueue),
|
||||
Stream.unwrapScoped,
|
||||
Stream.runForEach(flow(
|
||||
Cause.failures,
|
||||
Chunk.findFirst(e => e._tag === "RequestError" || e._tag === "ResponseError"),
|
||||
Option.match({
|
||||
onSome: e => Console.log("ResultView HttpClient error", e),
|
||||
onNone: () => Effect.void,
|
||||
}),
|
||||
)),
|
||||
Effect.forkScoped,
|
||||
))
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{Match.value(result).pipe(
|
||||
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||
Match.tag("Success", result => <>
|
||||
<Heading>{result.value.title}</Heading>
|
||||
<Text>{result.value.body}</Text>
|
||||
</>),
|
||||
Match.tag("Failure", result =>
|
||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||
),
|
||||
Match.orElse(() => <></>),
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
|
||||
export const Route = createFileRoute("/result")({
|
||||
component: Component.withRuntime(ResultView, runtime.context)
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, Effect, Match, Option, Ref, Runtime, Schema, Stream } from "effect"
|
||||
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
|
||||
import { Component, Form, Subscribable } from "effect-fc"
|
||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||
import { FaDeleteLeft } from "react-icons/fa6"
|
||||
@@ -31,7 +31,6 @@ export type TodoProps = (
|
||||
)
|
||||
|
||||
export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoProps) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
const state = yield* TodosState
|
||||
|
||||
const [
|
||||
@@ -55,17 +54,15 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
|
||||
Match.exhaustive,
|
||||
)
|
||||
),
|
||||
onSubmit: function(todo) {
|
||||
return Match.value(props).pipe(
|
||||
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
|
||||
Effect.andThen(makeTodo),
|
||||
Effect.andThen(Schema.encode(TodoFormSchema)),
|
||||
Effect.andThen(v => Ref.set(this.encodedValueRef, v)),
|
||||
)),
|
||||
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
|
||||
Match.exhaustive,
|
||||
)
|
||||
},
|
||||
f: ([todo, form]) => Match.value(props).pipe(
|
||||
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
|
||||
Effect.andThen(makeTodo),
|
||||
Effect.andThen(Schema.encode(TodoFormSchema)),
|
||||
Effect.andThen(v => Ref.set(form.encodedValue, v)),
|
||||
)),
|
||||
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
|
||||
Match.exhaustive,
|
||||
),
|
||||
autosubmit: props._tag === "edit",
|
||||
debounce: "250 millis",
|
||||
})
|
||||
@@ -73,17 +70,19 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
|
||||
return [
|
||||
indexRef,
|
||||
form,
|
||||
Form.field(form, ["content"]),
|
||||
Form.field(form, ["completedAt"]),
|
||||
yield* form.field(["content"]),
|
||||
yield* form.field(["completedAt"]),
|
||||
] as const
|
||||
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||
|
||||
const [index, size, canSubmit] = yield* Subscribable.useSubscribables(
|
||||
const [index, size, canSubmit] = yield* Subscribable.useSubscribables([
|
||||
indexRef,
|
||||
state.sizeSubscribable,
|
||||
form.canSubmitSubscribable,
|
||||
)
|
||||
const submit = yield* Form.useSubmit(form)
|
||||
form.canSubmit,
|
||||
])
|
||||
|
||||
const runSync = yield* Component.useRunSync()
|
||||
const runPromise = yield* Component.useRunPromise<DateTime.CurrentTimeZone>()
|
||||
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||
|
||||
|
||||
@@ -102,7 +101,7 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
|
||||
/>
|
||||
|
||||
{props._tag === "new" &&
|
||||
<Button disabled={!canSubmit} onClick={() => submit()}>
|
||||
<Button disabled={!canSubmit} onClick={() => void runPromise(form.submit)}>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
@@ -114,19 +113,19 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
|
||||
<Flex direction="column" justify="center" align="center" gap="1">
|
||||
<IconButton
|
||||
disabled={index <= 0}
|
||||
onClick={() => Runtime.runSync(runtime)(state.moveLeft(props.id))}
|
||||
onClick={() => runSync(state.moveLeft(props.id))}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={index >= size - 1}
|
||||
onClick={() => Runtime.runSync(runtime)(state.moveRight(props.id))}
|
||||
onClick={() => runSync(state.moveRight(props.id))}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={() => Runtime.runSync(runtime)(state.remove(props.id))}>
|
||||
<IconButton onClick={() => runSync(state.remove(props.id))}>
|
||||
<FaDeleteLeft />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TodosState } from "./TodosState.service"
|
||||
|
||||
export class Todos extends Component.makeUntraced("Todos")(function*() {
|
||||
const state = yield* TodosState
|
||||
const [todos] = yield* Subscribable.useSubscribables(state.ref)
|
||||
const [todos] = yield* Subscribable.useSubscribables([state.ref])
|
||||
|
||||
yield* Component.useOnMount(() => Effect.andThen(
|
||||
Console.log("Todos mounted"),
|
||||
|
||||
Reference in New Issue
Block a user