0.2.5 (#43)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud> Reviewed-on: #43
This commit was merged in pull request #43.
This commit is contained in:
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@effect-fc/monorepo",
|
"name": "@effect-fc/monorepo",
|
||||||
"packageManager": "bun@1.3.6",
|
"packageManager": "bun@1.3.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./packages/*"
|
"./packages/*"
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
"clean:modules": "turbo clean:modules && rm -rf node_modules"
|
"clean:modules": "turbo clean:modules && rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.11",
|
"@biomejs/biome": "^2.4.9",
|
||||||
"@effect/language-service": "^0.75.0",
|
"@effect/language-service": "^0.84.2",
|
||||||
"@types/bun": "^1.3.6",
|
"@types/bun": "^1.3.11",
|
||||||
"npm-check-updates": "^19.3.1",
|
"npm-check-updates": "^19.6.6",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
"turbo": "^2.7.5",
|
"turbo": "^2.8.21",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^6.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"@docusaurus/module-type-aliases": "3.9.2",
|
"@docusaurus/module-type-aliases": "3.9.2",
|
||||||
"@docusaurus/tsconfig": "3.9.2",
|
"@docusaurus/tsconfig": "3.9.2",
|
||||||
"@docusaurus/types": "3.9.2",
|
"@docusaurus/types": "3.9.2",
|
||||||
"typescript": "~5.6.2"
|
"typescript": "~6.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-fc",
|
"name": "effect-fc",
|
||||||
"description": "Write React function components with Effect",
|
"description": "Write React function components with Effect",
|
||||||
"version": "0.2.4",
|
"version": "0.2.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -38,11 +38,14 @@
|
|||||||
"clean:modules": "rm -rf node_modules"
|
"clean:modules": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/platform-browser": "^0.74.0"
|
"@effect/platform-browser": "^0.76.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "^3.19.0",
|
"effect": "^3.21.0",
|
||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"effect-lens": "^0.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,101 @@
|
|||||||
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 { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, Function, identity, Option, ParseResult, Pipeable, Predicate, Schema, type Scope, Stream } from "effect"
|
||||||
import type * as React from "react"
|
import type * as React from "react"
|
||||||
import * as Component from "./Component.js"
|
import * as Component from "./Component.js"
|
||||||
|
import * as Lens from "./Lens.js"
|
||||||
import * as Mutation from "./Mutation.js"
|
import * as Mutation from "./Mutation.js"
|
||||||
import * as PropertyPath from "./PropertyPath.js"
|
|
||||||
import * as Result from "./Result.js"
|
import * as Result from "./Result.js"
|
||||||
import * as Subscribable from "./Subscribable.js"
|
import * as Subscribable from "./Subscribable.js"
|
||||||
import * as SubscriptionRef from "./SubscriptionRef.js"
|
import * as SubscriptionRef from "./SubscriptionRef.js"
|
||||||
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
||||||
export type FormTypeId = typeof FormTypeId
|
export type FormTypeId = typeof FormTypeId
|
||||||
|
|
||||||
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>
|
export interface Form<out P extends readonly PropertyKey[], in out A, in out I = A, in out ER = never, in out EW = never>
|
||||||
extends Pipeable.Pipeable {
|
extends Pipeable.Pipeable {
|
||||||
readonly [FormTypeId]: FormTypeId
|
readonly [FormTypeId]: FormTypeId
|
||||||
|
|
||||||
|
readonly path: P
|
||||||
|
readonly value: Subscribable.Subscribable<Option.Option<A>, ER, never>
|
||||||
|
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>
|
||||||
|
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[], never, never>
|
||||||
|
readonly isValidating: Subscribable.Subscribable<boolean, ER, never>
|
||||||
|
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>
|
||||||
|
readonly isSubmitting: Subscribable.Subscribable<boolean, never, never>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormImpl<out P extends readonly PropertyKey[], in out A, in out I = A, in out ER = never, in out EW = never>
|
||||||
|
extends Pipeable.Class() implements Form<P, A, I, ER, EW> {
|
||||||
|
readonly [FormTypeId]: FormTypeId = FormTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly path: P,
|
||||||
|
readonly value: Subscribable.Subscribable<Option.Option<A>, ER, never>,
|
||||||
|
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>,
|
||||||
|
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[], never, never>,
|
||||||
|
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
|
||||||
|
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>,
|
||||||
|
readonly isSubmitting: Subscribable.Subscribable<boolean, never, never>,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const RootFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/RootForm")
|
||||||
|
export type RootFormTypeId = typeof RootFormTypeId
|
||||||
|
|
||||||
|
export interface RootForm<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 Form<readonly [], A, I, never, never> {
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
readonly context: Context.Context<Scope.Scope | R>
|
readonly context: Context.Context<Scope.Scope | R>
|
||||||
readonly mutation: Mutation.Mutation<
|
readonly mutation: Mutation.Mutation<
|
||||||
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
readonly [value: A, form: RootForm<A, I, R, unknown, unknown, unknown>],
|
||||||
MA, ME, MR, MP
|
MA, ME, MR, MP
|
||||||
>
|
>
|
||||||
readonly autosubmit: boolean
|
readonly autosubmit: boolean
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>
|
|
||||||
|
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>>
|
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never>
|
||||||
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 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 run: Effect.Effect<void>
|
readonly run: Effect.Effect<void>
|
||||||
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
|
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
export class RootFormImpl<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> {
|
extends Pipeable.Class() implements RootForm<A, I, R, MA, ME, MR, MP> {
|
||||||
readonly [FormTypeId]: FormTypeId = FormTypeId
|
readonly [FormTypeId]: FormTypeId = FormTypeId
|
||||||
|
readonly [RootFormTypeId]: RootFormTypeId = RootFormTypeId
|
||||||
|
|
||||||
|
readonly path = [] as const
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly schema: Schema.Schema<A, I, R>,
|
readonly schema: Schema.Schema<A, I, R>,
|
||||||
readonly context: Context.Context<Scope.Scope | R>,
|
readonly context: Context.Context<Scope.Scope | R>,
|
||||||
readonly mutation: Mutation.Mutation<
|
readonly mutation: Mutation.Mutation<
|
||||||
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
readonly [value: A, form: RootForm<A, I, R, unknown, unknown, unknown>],
|
||||||
MA, ME, MR, MP
|
MA, ME, MR, MP
|
||||||
>,
|
>,
|
||||||
readonly autosubmit: boolean,
|
readonly autosubmit: boolean,
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>,
|
|
||||||
|
|
||||||
readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
readonly value: Lens.Lens<Option.Option<A>, never, never, never, never>,
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
readonly encodedValue: Lens.Lens<I, never, never, never, never>,
|
||||||
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
readonly issues: Lens.Lens<readonly ParseResult.ArrayFormatterIssue[], never, never, never, never>,
|
||||||
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
|
readonly validationFiber: Lens.Lens<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never, never, never>,
|
||||||
|
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
|
||||||
|
|
||||||
|
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>,
|
||||||
|
readonly isSubmitting: Subscribable.Subscribable<boolean, never, never>,
|
||||||
|
|
||||||
readonly runSemaphore: Effect.Semaphore,
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
|
|
||||||
) {
|
) {
|
||||||
super()
|
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.hasRefreshingFlag(result))
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
field<const P extends PropertyPath.Paths<I>>(
|
|
||||||
path: P
|
|
||||||
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>> {
|
|
||||||
const key = new FormFieldKey(path)
|
|
||||||
return this.fieldCache.pipe(
|
|
||||||
Effect.map(HashMap.get(key)),
|
|
||||||
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(key, v as FormField<unknown, unknown>)),
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly canSubmit: Subscribable.Subscribable<boolean>
|
|
||||||
|
|
||||||
get run(): Effect.Effect<void> {
|
get run(): Effect.Effect<void> {
|
||||||
return this.runSemaphore.withPermits(1)(Stream.runForEach(
|
return this.runSemaphore.withPermits(1)(Stream.runForEach(
|
||||||
this.encodedValue.changes.pipe(
|
this.encodedValue.changes,
|
||||||
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
|
|
||||||
),
|
|
||||||
|
|
||||||
encodedValue => this.validationFiber.pipe(
|
encodedValue => Lens.get(this.validationFiber).pipe(
|
||||||
Effect.andThen(Option.match({
|
Effect.andThen(Option.match({
|
||||||
onSome: Fiber.interrupt,
|
onSome: Fiber.interrupt,
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
@@ -110,18 +106,21 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
exit => Effect.andThen(
|
exit => Effect.andThen(
|
||||||
Exit.matchEffect(exit, {
|
Exit.matchEffect(exit, {
|
||||||
onSuccess: v => Effect.andThen(
|
onSuccess: v => Effect.andThen(
|
||||||
Ref.set(this.value, Option.some(v)),
|
Lens.set(this.value, Option.some(v)),
|
||||||
Ref.set(this.error, Option.none()),
|
Lens.set(this.issues, Array.empty()),
|
||||||
),
|
),
|
||||||
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
||||||
onSome: e => Ref.set(this.error, Option.some(e)),
|
onSome: e => Effect.flatMap(
|
||||||
|
ParseResult.ArrayFormatter.formatError(e),
|
||||||
|
v => Lens.set(this.issues, v),
|
||||||
|
),
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
Ref.set(this.validationFiber, Option.none()),
|
Lens.set(this.validationFiber, Option.none()),
|
||||||
),
|
),
|
||||||
)).pipe(
|
)).pipe(
|
||||||
Effect.tap(fiber => Ref.set(this.validationFiber, Option.some(fiber))),
|
Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))),
|
||||||
Effect.andThen(Fiber.join),
|
Effect.andThen(Fiber.join),
|
||||||
Effect.andThen(value => this.autosubmit
|
Effect.andThen(value => this.autosubmit
|
||||||
? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
|
? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
|
||||||
@@ -136,7 +135,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
||||||
return this.value.pipe(
|
return Lens.get(this.value).pipe(
|
||||||
Effect.andThen(identity),
|
Effect.andThen(identity),
|
||||||
Effect.andThen(value => this.submitValue(value)),
|
Effect.andThen(value => this.submitValue(value)),
|
||||||
)
|
)
|
||||||
@@ -153,7 +152,10 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
e => e._tag === "ParseError",
|
e => e._tag === "ParseError",
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
onSome: e => Ref.set(this.error, Option.some(e)),
|
onSome: e => Effect.flatMap(
|
||||||
|
ParseResult.ArrayFormatter.formatError(e),
|
||||||
|
v => Lens.set(this.issues, v),
|
||||||
|
),
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -164,42 +166,59 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
|
||||||
|
export const isForm = (u: unknown): u is Form<readonly PropertyKey[], unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
||||||
|
export const isRootForm = (u: unknown): u is RootForm<readonly PropertyKey[], unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, RootFormTypeId)
|
||||||
|
|
||||||
|
|
||||||
export declare namespace make {
|
export declare namespace make {
|
||||||
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>
|
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<
|
extends Mutation.make.Options<
|
||||||
readonly [value: NoInfer<A>, form: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
|
readonly [value: NoInfer<A>, form: RootForm<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
|
||||||
MA, ME, MR, MP
|
MA, ME, MR, MP
|
||||||
> {
|
> {
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
readonly initialEncodedValue: NoInfer<I>
|
readonly initialEncodedValue: NoInfer<I>
|
||||||
readonly autosubmit?: boolean
|
readonly autosubmit?: boolean
|
||||||
readonly debounce?: Duration.DurationInput
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
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>
|
options: make.Options<A, I, R, MA, ME, MR, MP>
|
||||||
): Effect.fn.Return<
|
): Effect.fn.Return<
|
||||||
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
RootForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
Scope.Scope | R | Result.forkEffect.OutputContext<MR, MP>
|
||||||
> {
|
> {
|
||||||
return new FormImpl(
|
const mutation = yield* Mutation.make(options)
|
||||||
|
const valueLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<A>()))
|
||||||
|
const issuesLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make<readonly ParseResult.ArrayFormatterIssue[]>(Array.empty()))
|
||||||
|
const validationFiberLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()))
|
||||||
|
|
||||||
|
return new RootFormImpl(
|
||||||
options.schema,
|
options.schema,
|
||||||
yield* Effect.context<Scope.Scope | R>(),
|
yield* Effect.context<Scope.Scope | R>(),
|
||||||
yield* Mutation.make(options),
|
mutation,
|
||||||
options.autosubmit ?? false,
|
options.autosubmit ?? false,
|
||||||
Option.fromNullable(options.debounce),
|
|
||||||
|
|
||||||
yield* SubscriptionRef.make(Option.none<A>()),
|
valueLens,
|
||||||
yield* SubscriptionRef.make(options.initialEncodedValue),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(options.initialEncodedValue)),
|
||||||
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
|
issuesLens,
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()),
|
validationFiberLens,
|
||||||
|
Subscribable.map(validationFiberLens, Option.isSome),
|
||||||
|
|
||||||
|
Subscribable.map(
|
||||||
|
Subscribable.zipLatestAll(valueLens, issuesLens, validationFiberLens, mutation.result),
|
||||||
|
([value, issues, validationFiber, result]) => (
|
||||||
|
Option.isSome(value) &&
|
||||||
|
Array.isEmptyReadonlyArray(issues) &&
|
||||||
|
Option.isNone(validationFiber) &&
|
||||||
|
!(Result.isRunning(result) || Result.hasRefreshingFlag(result))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Subscribable.map(mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)),
|
||||||
|
|
||||||
yield* Effect.makeSemaphore(1),
|
yield* Effect.makeSemaphore(1),
|
||||||
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -211,83 +230,125 @@ export declare namespace service {
|
|||||||
export const service = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
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>
|
options: service.Options<A, I, R, MA, ME, MR, MP>
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
RootForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
Scope.Scope | R | Result.forkEffect.OutputContext<MR, MP>
|
||||||
> => Effect.tap(
|
> => Effect.tap(
|
||||||
make(options),
|
make(options),
|
||||||
form => Effect.forkScoped(form.run),
|
form => Effect.forkScoped(form.run),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField")
|
const filterIssuesByPath = (
|
||||||
export type FormFieldTypeId = typeof FormFieldTypeId
|
issues: readonly ParseResult.ArrayFormatterIssue[],
|
||||||
|
path: readonly PropertyKey[],
|
||||||
|
): readonly ParseResult.ArrayFormatterIssue[] => Array.filter(issues, issue =>
|
||||||
|
issue.path.length >= path.length && Array.every(path, (p, i) => p === issue.path[i])
|
||||||
|
)
|
||||||
|
|
||||||
export interface FormField<in out A, in out I = A>
|
export const focusObjectField: {
|
||||||
extends Pipeable.Pipeable {
|
<P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>(
|
||||||
readonly [FormFieldTypeId]: FormFieldTypeId
|
self: Form<P, A, I, ER, EW>,
|
||||||
|
key: K,
|
||||||
|
): Form<readonly [...P, K], A[K], I[K], ER, EW>
|
||||||
|
<P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>(
|
||||||
|
key: K,
|
||||||
|
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, K], A[K], I[K], ER, EW>
|
||||||
|
} = Function.dual(2, <P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>(
|
||||||
|
self: Form<P, A, I, ER, EW>,
|
||||||
|
key: K,
|
||||||
|
): Form<readonly [...P, K], A[K], I[K], ER, EW> => {
|
||||||
|
const form = self as FormImpl<P, A, I, ER, EW>
|
||||||
|
const path = [...form.path, key] as const
|
||||||
|
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
|
return new FormImpl(
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
path,
|
||||||
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
Subscribable.mapOption(form.value, a => a[key]),
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean>
|
Lens.focusObjectField(form.encodedValue, key),
|
||||||
readonly isSubmitting: Subscribable.Subscribable<boolean>
|
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
||||||
}
|
form.isValidating,
|
||||||
|
form.canSubmit,
|
||||||
class FormFieldImpl<in out A, in out I = A>
|
form.isSubmitting,
|
||||||
extends Pipeable.Class() implements FormField<A, I> {
|
|
||||||
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
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 Hash.array(this.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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>> => {
|
|
||||||
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.hasRefreshingFlag(result)),
|
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
export const focusArrayAt: {
|
||||||
|
<P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
|
||||||
|
self: Form<P, A, I, ER, EW>,
|
||||||
|
index: number,
|
||||||
|
): Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException>
|
||||||
|
<P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
|
||||||
|
index: number,
|
||||||
|
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException>
|
||||||
|
} = Function.dual(2, <P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
|
||||||
|
self: Form<P, A, I, ER, EW>,
|
||||||
|
index: number,
|
||||||
|
): Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException> => {
|
||||||
|
const form = self as FormImpl<P, A, I, ER, EW>
|
||||||
|
const path = [...form.path, index] as const
|
||||||
|
|
||||||
|
return new FormImpl(
|
||||||
|
path,
|
||||||
|
Subscribable.mapOptionEffect(form.value, Array.get(index)),
|
||||||
|
Lens.focusArrayAt(form.encodedValue, index),
|
||||||
|
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
||||||
|
form.isValidating,
|
||||||
|
form.canSubmit,
|
||||||
|
form.isSubmitting,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const focusTupleAt: {
|
||||||
|
<P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
|
||||||
|
self: Form<P, A, I, ER, EW>,
|
||||||
|
index: K,
|
||||||
|
): Form<readonly [...P, K], A[K], I[K], ER, EW>
|
||||||
|
<P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
|
||||||
|
index: K,
|
||||||
|
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, K], A[K], I[K], ER, EW>
|
||||||
|
} = Function.dual(2, <P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
|
||||||
|
self: Form<P, A, I, ER, EW>,
|
||||||
|
index: K,
|
||||||
|
): Form<readonly [...P, K], A[K], I[K], ER, EW> => {
|
||||||
|
const form = self as FormImpl<P, A, I, ER, EW>
|
||||||
|
const path = [...form.path, index] as const
|
||||||
|
|
||||||
|
return new FormImpl(
|
||||||
|
path,
|
||||||
|
Subscribable.mapOption(form.value, Array.unsafeGet(index)),
|
||||||
|
Lens.focusTupleAt(form.encodedValue, index),
|
||||||
|
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
||||||
|
form.isValidating,
|
||||||
|
form.canSubmit,
|
||||||
|
form.isSubmitting,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const focusChunkAt: {
|
||||||
|
<P extends readonly PropertyKey[], A, I, ER, EW>(
|
||||||
|
self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>,
|
||||||
|
index: number,
|
||||||
|
): Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW>
|
||||||
|
<P extends readonly PropertyKey[], A, I, ER, EW>(
|
||||||
|
index: number,
|
||||||
|
): (self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>) => Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW>
|
||||||
|
} = Function.dual(2, <P extends readonly PropertyKey[], A, I, ER, EW>(
|
||||||
|
self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>,
|
||||||
|
index: number,
|
||||||
|
): Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW> => {
|
||||||
|
const form = self as FormImpl<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>
|
||||||
|
const path = [...form.path, index] as const
|
||||||
|
|
||||||
|
return new FormImpl(
|
||||||
|
path,
|
||||||
|
Subscribable.mapOptionEffect(form.value, Chunk.get(index)),
|
||||||
|
Lens.focusChunkAt(form.encodedValue, index),
|
||||||
|
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
|
||||||
|
form.isValidating,
|
||||||
|
form.canSubmit,
|
||||||
|
form.isSubmitting,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
export namespace useInput {
|
export namespace useInput {
|
||||||
@@ -301,33 +362,39 @@ export namespace useInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useInput = Effect.fnUntraced(function* <A, I>(
|
export const useInput = Effect.fnUntraced(function* <P extends readonly PropertyKey[], A, I, ER, EW>(
|
||||||
field: FormField<A, I>,
|
form: Form<P, A, I, ER, EW>,
|
||||||
options?: useInput.Options,
|
options?: useInput.Options,
|
||||||
): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
): Effect.fn.Return<useInput.Success<I>, ER, Scope.Scope> {
|
||||||
const internalValueRef = yield* Component.useOnChange(() => Effect.tap(
|
const internalValueLens = yield* Component.useOnChange(() => Effect.gen(function*() {
|
||||||
Effect.andThen(field.encodedValue, SubscriptionRef.make),
|
const internalValueLens = yield* Lens.get(form.encodedValue).pipe(
|
||||||
internalValueRef => Effect.forkScoped(Effect.all([
|
Effect.flatMap(SubscriptionRef.make),
|
||||||
|
Effect.map(Lens.fromSubscriptionRef),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Effect.forkScoped(Effect.all([
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
Stream.drop(field.encodedValue, 1),
|
Stream.drop(form.encodedValue.changes, 1),
|
||||||
upstreamEncodedValue => Effect.whenEffect(
|
upstreamEncodedValue => Effect.whenEffect(
|
||||||
Ref.set(internalValueRef, upstreamEncodedValue),
|
Lens.set(internalValueLens, upstreamEncodedValue),
|
||||||
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
Effect.andThen(Lens.get(internalValueLens), internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
internalValueRef.changes.pipe(
|
internalValueLens.changes.pipe(
|
||||||
Stream.drop(1),
|
Stream.drop(1),
|
||||||
Stream.changesWith(Equal.equivalence()),
|
Stream.changesWith(Equal.equivalence()),
|
||||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||||
),
|
),
|
||||||
internalValue => Ref.set(field.encodedValue, internalValue),
|
internalValue => Lens.set(form.encodedValue, internalValue),
|
||||||
),
|
),
|
||||||
], { concurrency: "unbounded" })),
|
], { concurrency: "unbounded" }))
|
||||||
), [field, options?.debounce])
|
|
||||||
|
|
||||||
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
|
return internalValueLens
|
||||||
|
}), [form, options?.debounce])
|
||||||
|
|
||||||
|
const [value, setValue] = yield* Lens.useState(internalValueLens)
|
||||||
return { value, setValue }
|
return { value, setValue }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -342,55 +409,63 @@ export namespace useOptionalInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
export const useOptionalInput = Effect.fnUntraced(function* <P extends readonly PropertyKey[], A, I, ER, EW>(
|
||||||
field: FormField<A, Option.Option<I>>,
|
field: Form<P, A, Option.Option<I>, ER, EW>,
|
||||||
options: useOptionalInput.Options<I>,
|
options: useOptionalInput.Options<I>,
|
||||||
): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
): Effect.fn.Return<useOptionalInput.Success<I>, ER, Scope.Scope> {
|
||||||
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap(
|
const [enabledLens, internalValueLens] = yield* Component.useOnChange(() => Effect.gen(function*() {
|
||||||
Effect.andThen(
|
const [enabledLens, internalValueLens] = yield* Effect.flatMap(
|
||||||
field.encodedValue,
|
Lens.get(field.encodedValue),
|
||||||
Option.match({
|
Option.match({
|
||||||
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
|
onSome: v => Effect.all([
|
||||||
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
|
Effect.map(SubscriptionRef.make(true), Lens.fromSubscriptionRef),
|
||||||
|
Effect.map(SubscriptionRef.make(v), Lens.fromSubscriptionRef),
|
||||||
|
]),
|
||||||
|
onNone: () => Effect.all([
|
||||||
|
Effect.map(SubscriptionRef.make(false), Lens.fromSubscriptionRef),
|
||||||
|
Effect.map(SubscriptionRef.make(options.defaultValue), Lens.fromSubscriptionRef),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
|
|
||||||
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([
|
yield* Effect.forkScoped(Effect.all([
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
Stream.drop(field.encodedValue, 1),
|
Stream.drop(field.encodedValue.changes, 1),
|
||||||
|
|
||||||
upstreamEncodedValue => Effect.whenEffect(
|
upstreamEncodedValue => Effect.whenEffect(
|
||||||
Option.match(upstreamEncodedValue, {
|
Option.match(upstreamEncodedValue, {
|
||||||
onSome: v => Effect.andThen(
|
onSome: v => Effect.andThen(
|
||||||
Ref.set(enabledRef, true),
|
Lens.set(enabledLens, true),
|
||||||
Ref.set(internalValueRef, v),
|
Lens.set(internalValueLens, v),
|
||||||
),
|
),
|
||||||
onNone: () => Effect.andThen(
|
onNone: () => Effect.andThen(
|
||||||
Ref.set(enabledRef, false),
|
Lens.set(enabledLens, false),
|
||||||
Ref.set(internalValueRef, options.defaultValue),
|
Lens.set(internalValueLens, options.defaultValue),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Effect.andThen(
|
Effect.andThen(
|
||||||
Effect.all([enabledRef, internalValueRef]),
|
Effect.all([Lens.get(enabledLens), Lens.get(internalValueLens)]),
|
||||||
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
enabledRef.changes.pipe(
|
enabledLens.changes.pipe(
|
||||||
Stream.zipLatest(internalValueRef.changes),
|
Stream.zipLatest(internalValueLens.changes),
|
||||||
Stream.drop(1),
|
Stream.drop(1),
|
||||||
Stream.changesWith(Equal.equivalence()),
|
Stream.changesWith(Equal.equivalence()),
|
||||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||||
),
|
),
|
||||||
([enabled, internalValue]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
([enabled, internalValue]) => Lens.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||||
),
|
),
|
||||||
], { concurrency: "unbounded" })),
|
], { concurrency: "unbounded" }))
|
||||||
), [field, options.debounce])
|
|
||||||
|
|
||||||
const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef)
|
return [enabledLens, internalValueLens] as const
|
||||||
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
|
}), [field, options.debounce])
|
||||||
|
|
||||||
|
const [enabled, setEnabled] = yield* Lens.useState(enabledLens)
|
||||||
|
const [value, setValue] = yield* Lens.useState(internalValueLens)
|
||||||
return { enabled, setEnabled, value, setValue }
|
return { enabled, setEnabled, value, setValue }
|
||||||
})
|
})
|
||||||
|
|||||||
63
packages/effect-fc/src/Lens.ts
Normal file
63
packages/effect-fc/src/Lens.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Effect, Equivalence, Stream } from "effect"
|
||||||
|
import { Lens } from "effect-lens"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as Component from "./Component.js"
|
||||||
|
import * as SetStateAction from "./SetStateAction.js"
|
||||||
|
import * as SubscriptionRef from "./SubscriptionRef.js"
|
||||||
|
|
||||||
|
|
||||||
|
export * from "effect-lens/Lens"
|
||||||
|
|
||||||
|
export declare namespace useState {
|
||||||
|
export interface Options<A> {
|
||||||
|
readonly equivalence?: Equivalence.Equivalence<A>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useState = Effect.fnUntraced(function* <A, ER, EW, RR, RW>(
|
||||||
|
lens: Lens.Lens<A, ER, EW, RR, RW>,
|
||||||
|
options?: useState.Options<NoInfer<A>>,
|
||||||
|
): Effect.fn.Return<readonly [A, React.Dispatch<React.SetStateAction<A>>], ER, RR | RW> {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => Lens.get(lens)))
|
||||||
|
|
||||||
|
yield* Component.useReactEffect(() => Effect.forkScoped(
|
||||||
|
Stream.runForEach(
|
||||||
|
Stream.changesWith(lens.changes, options?.equivalence ?? Equivalence.strict()),
|
||||||
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
)
|
||||||
|
), [lens])
|
||||||
|
|
||||||
|
const setValue = yield* Component.useCallbackSync(
|
||||||
|
(setStateAction: React.SetStateAction<A>) => Effect.andThen(
|
||||||
|
Lens.updateAndGet(lens, prevState => SetStateAction.value(setStateAction, prevState)),
|
||||||
|
v => setReactStateValue(v),
|
||||||
|
),
|
||||||
|
[lens],
|
||||||
|
)
|
||||||
|
|
||||||
|
return [reactStateValue, setValue]
|
||||||
|
})
|
||||||
|
|
||||||
|
export declare namespace useFromState {
|
||||||
|
export interface Options<A> {
|
||||||
|
readonly equivalence?: Equivalence.Equivalence<A>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFromState = Effect.fnUntraced(function* <A>(
|
||||||
|
[value, setValue]: readonly [A, React.Dispatch<React.SetStateAction<A>>],
|
||||||
|
options?: useFromState.Options<NoInfer<A>>,
|
||||||
|
): Effect.fn.Return<Lens.Lens<A, never, never, never, never>> {
|
||||||
|
const lens = yield* Component.useOnMount(() => Effect.map(
|
||||||
|
SubscriptionRef.make(value),
|
||||||
|
Lens.fromSubscriptionRef,
|
||||||
|
))
|
||||||
|
|
||||||
|
yield* Component.useReactEffect(() => Effect.forkScoped(Stream.runForEach(
|
||||||
|
Stream.changesWith(lens.changes, options?.equivalence ?? Equivalence.strict()),
|
||||||
|
v => Effect.sync(() => setValue(v)),
|
||||||
|
)), [setValue])
|
||||||
|
yield* Component.useReactEffect(() => Lens.set(lens, value), [value])
|
||||||
|
|
||||||
|
return lens
|
||||||
|
})
|
||||||
@@ -111,12 +111,12 @@ export declare namespace make {
|
|||||||
export const make = Effect.fnUntraced(function* <const K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never>(
|
export const make = Effect.fnUntraced(function* <const K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never>(
|
||||||
options: make.Options<K, A, E, R, P>
|
options: make.Options<K, A, E, R, P>
|
||||||
): Effect.fn.Return<
|
): Effect.fn.Return<
|
||||||
Mutation<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
Mutation<K, A, E, Result.forkEffect.OutputContext<R, P>, P>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | Result.forkEffect.OutputContext<R, P>
|
||||||
> {
|
> {
|
||||||
return new MutationImpl(
|
return new MutationImpl(
|
||||||
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<R, P>>(),
|
||||||
options.f as any,
|
options.f as any,
|
||||||
options.initialProgress as P,
|
options.initialProgress as P,
|
||||||
|
|
||||||
|
|||||||
67
packages/effect-fc/src/PropertyPath.test.ts
Normal file
67
packages/effect-fc/src/PropertyPath.test.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Option } from "effect"
|
||||||
|
import * as PropertyPath from "./PropertyPath.js"
|
||||||
|
|
||||||
|
|
||||||
|
describe("immutableSet with arrays", () => {
|
||||||
|
test("sets a top-level array element", () => {
|
||||||
|
const arr = [1, 2, 3]
|
||||||
|
const result = PropertyPath.immutableSet(arr, [1], 99)
|
||||||
|
expect(result).toEqual(Option.some([1, 99, 3]))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not mutate the original array", () => {
|
||||||
|
const arr = [1, 2, 3]
|
||||||
|
PropertyPath.immutableSet(arr, [0], 42)
|
||||||
|
expect(arr).toEqual([1, 2, 3])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sets the first element of an array", () => {
|
||||||
|
const arr = ["a", "b", "c"]
|
||||||
|
const result = PropertyPath.immutableSet(arr, [0], "z")
|
||||||
|
expect(result).toEqual(Option.some(["z", "b", "c"]))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sets the last element of an array", () => {
|
||||||
|
const arr = [10, 20, 30]
|
||||||
|
const result = PropertyPath.immutableSet(arr, [2], 99)
|
||||||
|
expect(result).toEqual(Option.some([10, 20, 99]))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sets a nested array element inside an object", () => {
|
||||||
|
const obj = { tags: ["foo", "bar", "baz"] }
|
||||||
|
const result = PropertyPath.immutableSet(obj, ["tags", 1], "qux")
|
||||||
|
expect(result).toEqual(Option.some({ tags: ["foo", "qux", "baz"] }))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sets a deeply nested value inside an array of objects", () => {
|
||||||
|
const obj = { items: [{ name: "alice" }, { name: "bob" }] }
|
||||||
|
const result = PropertyPath.immutableSet(obj, ["items", 0, "name"], "charlie")
|
||||||
|
expect(result).toEqual(Option.some({ items: [{ name: "charlie" }, { name: "bob" }] }))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sets a value in a nested array", () => {
|
||||||
|
const matrix = [[1, 2], [3, 4]]
|
||||||
|
const result = PropertyPath.immutableSet(matrix, [1, 0], 99)
|
||||||
|
expect(result).toEqual(Option.some([[1, 2], [99, 4]]))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns Option.none() for an out-of-bounds index", () => {
|
||||||
|
const arr = [1, 2, 3]
|
||||||
|
const result = PropertyPath.immutableSet(arr, [5], 99)
|
||||||
|
expect(result).toEqual(Option.none())
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns Option.none() for a non-numeric key on an array", () => {
|
||||||
|
const arr = [1, 2, 3]
|
||||||
|
// @ts-expect-error intentionally wrong key type
|
||||||
|
const result = PropertyPath.immutableSet(arr, ["length"], 0)
|
||||||
|
expect(result).toEqual(Option.none())
|
||||||
|
})
|
||||||
|
|
||||||
|
test("empty path returns Option.some of the value itself", () => {
|
||||||
|
const arr = [1, 2, 3]
|
||||||
|
const result = PropertyPath.immutableSet(arr, [], [9, 9, 9] as any)
|
||||||
|
expect(result).toEqual(Option.some([9, 9, 9]))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -281,14 +281,14 @@ export declare namespace make {
|
|||||||
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
|
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
|
||||||
options: make.Options<K, A, KE, KR, E, R, P>
|
options: make.Options<K, A, KE, KR, E, R, P>
|
||||||
): Effect.fn.Return<
|
): Effect.fn.Return<
|
||||||
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>
|
||||||
> {
|
> {
|
||||||
const client = yield* QueryClient.QueryClient
|
const client = yield* QueryClient.QueryClient
|
||||||
|
|
||||||
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>(
|
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>(
|
||||||
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>>(),
|
||||||
options.key,
|
options.key,
|
||||||
options.f as any,
|
options.f as any,
|
||||||
options.initialProgress as P,
|
options.initialProgress as P,
|
||||||
@@ -308,9 +308,9 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE =
|
|||||||
export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
|
export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
|
||||||
options: make.Options<K, A, KE, KR, E, R, P>
|
options: make.Options<K, A, KE, KR, E, R, P>
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>
|
||||||
> => Effect.tap(
|
> => Effect.tap(
|
||||||
make(options),
|
make(options),
|
||||||
query => Effect.forkScoped(query.run),
|
query => Effect.forkScoped(query.run),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Pipeable, Predicate, PubSub, pipe, Ref, type Scope, Stream, Subscribable } from "effect"
|
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, pipe, Pipeable, Predicate, PubSub, Ref, type Scope, Stream, type Subscribable, SynchronizedRef } from "effect"
|
||||||
|
import { Lens } from "effect-lens"
|
||||||
|
|
||||||
|
|
||||||
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
||||||
@@ -15,10 +16,6 @@ export type Final<A, E = never, P = never> = (Success<A> | Failure<E>) & ({} | F
|
|||||||
export type Flags<P = never> = WillFetch | WillRefresh | Refreshing<P>
|
export type Flags<P = never> = WillFetch | WillRefresh | Refreshing<P>
|
||||||
|
|
||||||
export declare namespace Result {
|
export declare 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 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 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 type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never
|
||||||
@@ -28,21 +25,21 @@ export declare namespace Flags {
|
|||||||
export type Keys = keyof WillFetch & WillRefresh & Refreshing<any>
|
export type Keys = keyof WillFetch & WillRefresh & Refreshing<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Initial extends Result.Prototype {
|
export interface Initial extends ResultPrototype {
|
||||||
readonly _tag: "Initial"
|
readonly _tag: "Initial"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Running<P = never> extends Result.Prototype {
|
export interface Running<P = never> extends ResultPrototype {
|
||||||
readonly _tag: "Running"
|
readonly _tag: "Running"
|
||||||
readonly progress: P
|
readonly progress: P
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Success<A> extends Result.Prototype {
|
export interface Success<A> extends ResultPrototype {
|
||||||
readonly _tag: "Success"
|
readonly _tag: "Success"
|
||||||
readonly value: A
|
readonly value: A
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Failure<E = never> extends Result.Prototype {
|
export interface Failure<E = never> extends ResultPrototype {
|
||||||
readonly _tag: "Failure"
|
readonly _tag: "Failure"
|
||||||
readonly cause: Cause.Cause<E>
|
readonly cause: Cause.Cause<E>
|
||||||
}
|
}
|
||||||
@@ -61,7 +58,11 @@ export interface Refreshing<P = never> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ResultPrototype = Object.freeze({
|
export interface ResultPrototype extends Pipeable.Pipeable, Equal.Equal {
|
||||||
|
readonly [ResultTypeId]: ResultTypeId
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResultPrototype: ResultPrototype = Object.freeze({
|
||||||
...Pipeable.Prototype,
|
...Pipeable.Prototype,
|
||||||
[ResultTypeId]: ResultTypeId,
|
[ResultTypeId]: ResultTypeId,
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ const ResultPrototype = Object.freeze({
|
|||||||
Hash.cached(this),
|
Hash.cached(this),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
} as const satisfies Result.Prototype)
|
} as const)
|
||||||
|
|
||||||
|
|
||||||
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
|
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
|
||||||
@@ -162,52 +163,40 @@ export const toExit: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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> {
|
export interface Progress<P = never> {
|
||||||
readonly update: <E, R>(
|
readonly progress: Lens.Lens<P, PreviousResultNotRunningNorRefreshing, never, never, never>
|
||||||
f: (previous: P) => Effect.Effect<P, E, R>
|
|
||||||
) => Effect.Effect<void, PreviousResultNotRunningNorRefreshing | E, R>
|
|
||||||
}
|
}
|
||||||
|
export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress")
|
||||||
|
|
||||||
export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{
|
export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{
|
||||||
readonly previous: Result<unknown, unknown, unknown>
|
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>(
|
||||||
|
state: Lens.Lens<Result<A, E, P>, never, never, never, never>
|
||||||
export const makeProgressLayer = <A, E, P = never>(): Layer.Layer<
|
): Layer.Layer<Progress<P> | Progress<never>, never, never> => Layer.succeed(
|
||||||
Progress<P>,
|
Progress<P>() as Context.Tag<Progress<P> | Progress<never>, Progress<P> | Progress<never>>,
|
||||||
never,
|
{
|
||||||
State<A, E, P>
|
progress: state.pipe(
|
||||||
> => Layer.effect(Progress<P>(), Effect.gen(function*() {
|
Lens.mapEffect(
|
||||||
const state = yield* State<A, E, P>()
|
a => (isRunning(a) || hasRefreshingFlag(a))
|
||||||
|
? Effect.succeed(a)
|
||||||
return {
|
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous: a })),
|
||||||
update: <FE, FR>(f: (previous: P) => Effect.Effect<P, FE, FR>) => Effect.Do.pipe(
|
(_, b) => Effect.succeed(b),
|
||||||
Effect.bind("previous", () => Effect.andThen(state.get, previous =>
|
|
||||||
(isRunning(previous) || hasRefreshingFlag(previous))
|
|
||||||
? Effect.succeed(previous)
|
|
||||||
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })),
|
|
||||||
)),
|
|
||||||
Effect.bind("progress", ({ previous }) => f(previous.progress)),
|
|
||||||
Effect.let("next", ({ previous, progress }) => isRunning(previous)
|
|
||||||
? running(progress)
|
|
||||||
: refreshing(previous, progress) as Final<A, E, P> & Refreshing<P>
|
|
||||||
),
|
),
|
||||||
Effect.andThen(({ next }) => state.set(next)),
|
Lens.map(
|
||||||
|
a => a.progress,
|
||||||
|
(a, b) => isRunning(a)
|
||||||
|
? running(b)
|
||||||
|
: refreshing(a, b) as Final<A, E, P> & Refreshing<P>,
|
||||||
),
|
),
|
||||||
}
|
)
|
||||||
}))
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
export namespace unsafeForkEffect {
|
export namespace unsafeForkEffect {
|
||||||
export type OutputContext<A, E, R, P> = Exclude<R, State<A, E, P> | Progress<P> | Progress<never>>
|
export type OutputContext<R, P> = Exclude<R, Progress<P> | Progress<never>>
|
||||||
|
|
||||||
export interface Options<A, E, P> {
|
export interface Options<A, E, P> {
|
||||||
readonly initial?: Initial | Final<A, E, P>
|
readonly initial?: Initial | Final<A, E, P>
|
||||||
@@ -215,55 +204,56 @@ export namespace unsafeForkEffect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unsafeForkEffect = <A, E, R, P = never>(
|
export const unsafeForkEffect = Effect.fnUntraced(function* <A, E, R, P = never>(
|
||||||
effect: Effect.Effect<A, E, R>,
|
effect: Effect.Effect<A, E, R>,
|
||||||
options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||||
): Effect.Effect<
|
): Effect.fn.Return<
|
||||||
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||||
never,
|
never,
|
||||||
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | unsafeForkEffect.OutputContext<R, P>
|
||||||
> => Effect.Do.pipe(
|
> {
|
||||||
Effect.bind("ref", () => Ref.make(options?.initial ?? initial<A, E, P>())),
|
const ref = yield* SynchronizedRef.make<Result<A, E, P>>(options?.initial ?? initial<A, E, P>())
|
||||||
Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()),
|
const pubsub = yield* PubSub.unbounded<Result<A, E, P>>()
|
||||||
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
|
|
||||||
Effect.andThen(state => state.set(
|
const state = Lens.make<Result<A, E, P>, never, never, never, never>({
|
||||||
(isFinal(options?.initial) && hasWillRefreshFlag(options?.initial))
|
get get() { return Ref.get(ref) },
|
||||||
? refreshing(options.initial, options?.initialProgress) as Result<A, E, P>
|
get changes() {
|
||||||
: running(options?.initialProgress)
|
return Stream.unwrapScoped(Effect.map(
|
||||||
).pipe(
|
|
||||||
Effect.andThen(effect),
|
|
||||||
Effect.onExit(exit => Effect.andThen(
|
|
||||||
state.set(fromExit(exit)),
|
|
||||||
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.get(ref),
|
|
||||||
set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v))
|
|
||||||
})),
|
|
||||||
)),
|
|
||||||
))),
|
|
||||||
Effect.map(({ ref, pubsub, fiber }) => [
|
|
||||||
Subscribable.make({
|
|
||||||
get: Ref.get(ref),
|
|
||||||
changes: Stream.unwrapScoped(Effect.map(
|
|
||||||
Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]),
|
Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]),
|
||||||
([latest, stream]) => Stream.concat(Stream.make(latest), stream),
|
([latest, stream]) => Stream.concat(Stream.make(latest), stream),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
modify: f => Ref.get(ref).pipe(
|
||||||
|
Effect.flatMap(f),
|
||||||
|
Effect.flatMap(([b, a]) => Ref.set(ref, a).pipe(
|
||||||
|
Effect.as(b),
|
||||||
|
Effect.zipLeft(PubSub.publish(pubsub, a))
|
||||||
)),
|
)),
|
||||||
}),
|
),
|
||||||
fiber,
|
})
|
||||||
]),
|
|
||||||
) as Effect.Effect<
|
const fiber = yield* Effect.gen(function*() {
|
||||||
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
yield* Lens.set(
|
||||||
never,
|
state,
|
||||||
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
|
(isFinal(options?.initial) && hasWillRefreshFlag(options?.initial))
|
||||||
>
|
? refreshing(options.initial, options?.initialProgress) as Result<A, E, P>
|
||||||
|
: running(options?.initialProgress),
|
||||||
|
)
|
||||||
|
return yield* Effect.onExit(effect, exit => Effect.andThen(
|
||||||
|
Lens.set(state, fromExit(exit)),
|
||||||
|
Effect.forkScoped(PubSub.shutdown(pubsub)),
|
||||||
|
))
|
||||||
|
}).pipe(
|
||||||
|
Effect.forkScoped,
|
||||||
|
Effect.provide(makeProgressLayer(state)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return [state, fiber] as const
|
||||||
|
})
|
||||||
|
|
||||||
export namespace forkEffect {
|
export namespace forkEffect {
|
||||||
export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R
|
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 OutputContext<R, P> = unsafeForkEffect.OutputContext<R, P>
|
||||||
export interface Options<A, E, P> extends unsafeForkEffect.Options<A, E, P> {}
|
export interface Options<A, E, P> extends unsafeForkEffect.Options<A, E, P> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +264,6 @@ export const forkEffect: {
|
|||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
|
||||||
never,
|
never,
|
||||||
Scope.Scope | forkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | forkEffect.OutputContext<R, P>
|
||||||
>
|
>
|
||||||
} = unsafeForkEffect
|
} = unsafeForkEffect
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type * as React from "react"
|
|||||||
|
|
||||||
|
|
||||||
export const value: {
|
export const value: {
|
||||||
<S>(prevState: S): (self: React.SetStateAction<S>) => S
|
|
||||||
<S>(self: React.SetStateAction<S>, prevState: S): S
|
<S>(self: React.SetStateAction<S>, prevState: S): S
|
||||||
|
<S>(prevState: S): (self: React.SetStateAction<S>) => S
|
||||||
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
|
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
|
||||||
typeof self === "function"
|
typeof self === "function"
|
||||||
? (self as (prevState: S) => S)(prevState)
|
? (self as (prevState: S) => S)(prevState)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Effect, Equivalence, Stream, Subscribable } from "effect"
|
import { Effect, Equivalence, Stream } from "effect"
|
||||||
|
import { Subscribable } from "effect-lens"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as Component from "./Component.js"
|
import * as Component from "./Component.js"
|
||||||
|
|
||||||
|
|
||||||
|
export * from "effect-lens/Subscribable"
|
||||||
|
|
||||||
export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||||
...elements: T
|
...elements: T
|
||||||
): Subscribable.Subscribable<
|
): Subscribable.Subscribable<
|
||||||
@@ -48,5 +51,3 @@ export const useSubscribables = Effect.fnUntraced(function* <const T extends rea
|
|||||||
|
|
||||||
return reactStateValue as any
|
return reactStateValue as any
|
||||||
})
|
})
|
||||||
|
|
||||||
export * from "effect/Subscribable"
|
|
||||||
|
|||||||
183
packages/effect-fc/src/SubscriptionSubRef.test.ts
Normal file
183
packages/effect-fc/src/SubscriptionSubRef.test.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Chunk, Effect, Ref, SubscriptionRef } from "effect"
|
||||||
|
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||||
|
|
||||||
|
|
||||||
|
describe("SubscriptionSubRef with array refs", () => {
|
||||||
|
test("creates a subref for a single array element using path", async () => {
|
||||||
|
const value = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make([{ name: "alice" }, { name: "bob" }, { name: "charlie" }]),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "name"])
|
||||||
|
return subref.get
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(value).toBe("bob")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("modifies a single array element via subref", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make([{ name: "alice" }, { name: "bob" }, { name: "charlie" }]),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "name"])
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.set(subref, "bob-updated"),
|
||||||
|
() => Ref.get(parent),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([{ name: "alice" }, { name: "bob-updated" }, { name: "charlie" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("modifies array element at index 0", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make([10, 20, 30]),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromPath(parent, [0])
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.set(subref, 99),
|
||||||
|
() => Ref.get(parent),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([99, 20, 30])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("modifies array element at last index", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make(["a", "b", "c"]),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromPath(parent, [2])
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.set(subref, "z"),
|
||||||
|
() => Ref.get(parent),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(["a", "b", "z"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("modifies nested array element", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make([[1, 2], [3, 4], [5, 6]]),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromPath(parent, [1, 0])
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.set(subref, 99),
|
||||||
|
() => Ref.get(parent),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([[1, 2], [99, 4], [5, 6]])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses modifyEffect to transform array element", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make([{ count: 1 }, { count: 2 }, { count: 3 }]),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "count"])
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.update(subref, count => count + 100),
|
||||||
|
() => Effect.map(Ref.get(parent), parentValue => ({ result: 102, parentValue })),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.result).toBe(102) // count + 100
|
||||||
|
expect(result.parentValue).toEqual([{ count: 1 }, { count: 102 }, { count: 3 }]) // count + 100
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses modify to transform array element", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make([10, 20, 30]),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromPath(parent, [1])
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.update(subref, x => x + 5),
|
||||||
|
() => Effect.map(Ref.get(parent), parentValue => ({ result: 25, parentValue })),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.result).toBe(25) // 20 + 5
|
||||||
|
expect(result.parentValue).toEqual([10, 25, 30]) // 20 + 5
|
||||||
|
})
|
||||||
|
|
||||||
|
test("makeFromChunkIndex modifies chunk element", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make(Chunk.make(100, 200, 300)),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromChunkIndex(parent, 1)
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.set(subref, 999),
|
||||||
|
() => Ref.get(parent),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(Chunk.toReadonlyArray(result)).toEqual([100, 999, 300])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("makeFromGetSet with custom getter/setter for array element", async () => {
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make([{ id: 1, value: "a" }, { id: 2, value: "b" }]),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromGetSet(parent, {
|
||||||
|
get: arr => arr[0].value,
|
||||||
|
set: (arr, newValue) => [
|
||||||
|
{ ...arr[0], value: newValue },
|
||||||
|
...arr.slice(1),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.set(subref, "updated"),
|
||||||
|
() => Ref.get(parent),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([{ id: 1, value: "updated" }, { id: 2, value: "b" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not mutate original array when modifying via subref", async () => {
|
||||||
|
const original = [{ name: "alice" }, { name: "bob" }]
|
||||||
|
const result = await Effect.runPromise(
|
||||||
|
Effect.flatMap(
|
||||||
|
SubscriptionRef.make(original),
|
||||||
|
parent => {
|
||||||
|
const subref = SubscriptionSubRef.makeFromPath(parent, [0, "name"])
|
||||||
|
return Effect.flatMap(
|
||||||
|
Ref.set(subref, "alice-updated"),
|
||||||
|
() => Ref.get(parent),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(original).toEqual([{ name: "alice" }, { name: "bob" }]) // original unchanged
|
||||||
|
expect(result).toEqual([{ name: "alice-updated" }, { name: "bob" }]) // new value in ref
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,6 +2,7 @@ export * as Async from "./Async.js"
|
|||||||
export * as Component from "./Component.js"
|
export * as Component from "./Component.js"
|
||||||
export * as ErrorObserver from "./ErrorObserver.js"
|
export * as ErrorObserver from "./ErrorObserver.js"
|
||||||
export * as Form from "./Form.js"
|
export * as Form from "./Form.js"
|
||||||
|
export * as Lens from "./Lens.js"
|
||||||
export * as Memoized from "./Memoized.js"
|
export * as Memoized from "./Memoized.js"
|
||||||
export * as Mutation from "./Mutation.js"
|
export * as Mutation from "./Mutation.js"
|
||||||
export * as PropertyPath from "./PropertyPath.js"
|
export * as PropertyPath from "./PropertyPath.js"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"noPropertyAccessFromIndexSignature": false,
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
// Build
|
// Build
|
||||||
|
"rootDir": "./src",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
@@ -34,5 +35,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": ["./src"]
|
"include": ["./src"],
|
||||||
|
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,30 +13,30 @@
|
|||||||
"clean:modules": "rm -rf node_modules"
|
"clean:modules": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/react-router": "^1.154.12",
|
"@tanstack/react-router": "^1.168.3",
|
||||||
"@tanstack/react-router-devtools": "^1.154.12",
|
"@tanstack/react-router-devtools": "^1.166.11",
|
||||||
"@tanstack/router-plugin": "^1.154.12",
|
"@tanstack/router-plugin": "^1.167.4",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.4.0",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"type-fest": "^5.4.1",
|
"type-fest": "^5.5.0",
|
||||||
"vite": "^7.3.1"
|
"vite": "^8.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform": "^0.94.2",
|
"@effect/platform": "^0.96.0",
|
||||||
"@effect/platform-browser": "^0.74.0",
|
"@effect/platform-browser": "^0.76.0",
|
||||||
"@radix-ui/themes": "^3.2.1",
|
"@radix-ui/themes": "^3.3.0",
|
||||||
"@typed/id": "^0.17.2",
|
"@typed/id": "^0.17.2",
|
||||||
"effect": "^3.19.15",
|
"effect": "^3.21.0",
|
||||||
"effect-fc": "workspace:*",
|
"effect-fc": "workspace:*",
|
||||||
"react-icons": "^5.5.0"
|
"react-icons": "^5.6.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.14",
|
||||||
"effect": "^3.19.15",
|
"effect": "^3.21.0",
|
||||||
"react": "^19.2.3"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes"
|
import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes"
|
||||||
import { Array, Option } from "effect"
|
import { Array, Option, Struct } from "effect"
|
||||||
import { Component, Form, Subscribable } from "effect-fc"
|
import { Component, Form, Subscribable } from "effect-fc"
|
||||||
|
|
||||||
|
|
||||||
export declare namespace TextFieldFormInputView {
|
export declare namespace TextFieldFormInputView {
|
||||||
export interface Props
|
export interface Props extends Omit<TextField.RootProps, "form">, Form.useInput.Options {
|
||||||
extends TextField.RootProps, Form.useInput.Options {
|
readonly form: Form.Form<readonly PropertyKey[], any, string>
|
||||||
readonly field: Form.FormField<any, string>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*(
|
export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*(
|
||||||
props: TextFieldFormInputView.Props
|
props: TextFieldFormInputView.Props
|
||||||
) {
|
) {
|
||||||
const input = yield* Form.useInput(props.field, props)
|
const input = yield* Form.useInput(props.form, props)
|
||||||
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
|
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
|
||||||
props.field.issues,
|
props.form.issues,
|
||||||
props.field.isValidating,
|
props.form.isValidating,
|
||||||
props.field.isSubmitting,
|
props.form.isSubmitting,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +25,7 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi
|
|||||||
value={input.value}
|
value={input.value}
|
||||||
onChange={e => input.setValue(e.target.value)}
|
onChange={e => input.setValue(e.target.value)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...props}
|
{...Struct.omit(props, "form")}
|
||||||
>
|
>
|
||||||
{isValidating &&
|
{isValidating &&
|
||||||
<TextField.Slot side="right">
|
<TextField.Slot side="right">
|
||||||
|
|||||||
@@ -4,21 +4,19 @@ import { Component, Form, Subscribable } from "effect-fc"
|
|||||||
|
|
||||||
|
|
||||||
export declare namespace TextFieldOptionalFormInputView {
|
export declare namespace TextFieldOptionalFormInputView {
|
||||||
export interface Props
|
export interface Props extends Omit<TextField.RootProps, "form" | "defaultValue">, Form.useOptionalInput.Options<string> {
|
||||||
extends Omit<TextField.RootProps, "defaultValue">, Form.useOptionalInput.Options<string> {
|
readonly form: Form.Form<readonly PropertyKey[], any, Option.Option<string>>
|
||||||
readonly field: Form.FormField<any, Option.Option<string>>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class TextFieldOptionalFormInputView extends Component.make("TextFieldOptionalFormInputView")(function*(
|
export class TextFieldOptionalFormInputView extends Component.make("TextFieldOptionalFormInputView")(function*(
|
||||||
props: TextFieldOptionalFormInputView.Props
|
props: TextFieldOptionalFormInputView.Props
|
||||||
) {
|
) {
|
||||||
const input = yield* Form.useOptionalInput(props.field, props)
|
const input = yield* Form.useOptionalInput(props.form, props)
|
||||||
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
|
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
|
||||||
props.field.issues,
|
props.form.issues,
|
||||||
props.field.isValidating,
|
props.form.isValidating,
|
||||||
props.field.isSubmitting,
|
props.form.isSubmitting,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +25,7 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt
|
|||||||
value={input.value}
|
value={input.value}
|
||||||
onChange={e => input.setValue(e.target.value)}
|
onChange={e => input.setValue(e.target.value)}
|
||||||
disabled={!input.enabled || isSubmitting}
|
disabled={!input.enabled || isSubmitting}
|
||||||
{...Struct.omit(props, "defaultValue")}
|
{...Struct.omit(props, "form", "defaultValue")}
|
||||||
>
|
>
|
||||||
<TextField.Slot side="left">
|
<TextField.Slot side="left">
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
|
||||||
import { Component, Form, Subscribable } from "effect-fc"
|
|
||||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||||
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
||||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||||
import { runtime } from "@/runtime"
|
import { runtime } from "@/runtime"
|
||||||
|
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||||
|
import { Component, Form, Subscribable } from "effect-fc"
|
||||||
|
|
||||||
|
|
||||||
const email = Schema.pattern<typeof Schema.String>(
|
const email = Schema.pattern<typeof Schema.String>(
|
||||||
@@ -40,7 +40,8 @@ const RegisterFormSubmitSchema = Schema.Struct({
|
|||||||
})
|
})
|
||||||
|
|
||||||
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
|
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
|
||||||
scoped: Form.service({
|
scoped: Effect.gen(function*() {
|
||||||
|
const form = yield* Form.service({
|
||||||
schema: RegisterFormSchema.pipe(
|
schema: RegisterFormSchema.pipe(
|
||||||
Schema.compose(
|
Schema.compose(
|
||||||
Schema.transformOrFail(
|
Schema.transformOrFail(
|
||||||
@@ -59,15 +60,22 @@ class RegisterFormService extends Effect.Service<RegisterFormService>()("Registe
|
|||||||
yield* Effect.sleep("500 millis")
|
yield* Effect.sleep("500 millis")
|
||||||
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
|
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
|
||||||
}),
|
}),
|
||||||
debounce: "500 millis",
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
emailField: Form.focusObjectField(form, "email"),
|
||||||
|
passwordField: Form.focusObjectField(form, "password"),
|
||||||
|
birthField: Form.focusObjectField(form, "birth"),
|
||||||
|
} as const
|
||||||
})
|
})
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
||||||
const form = yield* RegisterFormService
|
const form = yield* RegisterFormService
|
||||||
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
||||||
form.canSubmit,
|
form.form.canSubmit,
|
||||||
form.mutation.result,
|
form.form.mutation.result,
|
||||||
])
|
])
|
||||||
|
|
||||||
const runPromise = yield* Component.useRunPromise()
|
const runPromise = yield* Component.useRunPromise()
|
||||||
@@ -84,20 +92,22 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
|||||||
<Container width="300">
|
<Container width="300">
|
||||||
<form onSubmit={e => {
|
<form onSubmit={e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
void runPromise(form.submit)
|
void runPromise(form.form.submit)
|
||||||
}}>
|
}}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<TextFieldFormInput
|
<TextFieldFormInput
|
||||||
field={yield* form.field(["email"])}
|
form={form.emailField}
|
||||||
|
debounce="250 millis"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextFieldFormInput
|
<TextFieldFormInput
|
||||||
field={yield* form.field(["password"])}
|
form={form.passwordField}
|
||||||
|
debounce="250 millis"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextFieldOptionalFormInput
|
<TextFieldOptionalFormInput
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
field={yield* form.field(["birth"])}
|
form={form.birthField}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
|
||||||
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"
|
|
||||||
import * as Domain from "@/domain"
|
import * as Domain from "@/domain"
|
||||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||||
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
||||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||||
|
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
|
||||||
|
import { Component, Form, Lens, Subscribable } from "effect-fc"
|
||||||
|
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||||
|
import { FaDeleteLeft } from "react-icons/fa6"
|
||||||
import { TodosState } from "./TodosState"
|
import { TodosState } from "./TodosState"
|
||||||
|
|
||||||
|
|
||||||
@@ -59,20 +59,19 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
|
|||||||
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
|
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
|
||||||
Effect.andThen(makeTodo),
|
Effect.andThen(makeTodo),
|
||||||
Effect.andThen(Schema.encode(TodoFormSchema)),
|
Effect.andThen(Schema.encode(TodoFormSchema)),
|
||||||
Effect.andThen(v => Ref.set(form.encodedValue, v)),
|
Effect.andThen(v => Lens.set(form.encodedValue, v)),
|
||||||
)),
|
)),
|
||||||
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
|
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
|
||||||
Match.exhaustive,
|
Match.exhaustive,
|
||||||
),
|
),
|
||||||
autosubmit: props._tag === "edit",
|
autosubmit: props._tag === "edit",
|
||||||
debounce: "250 millis",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return [
|
return [
|
||||||
indexRef,
|
indexRef,
|
||||||
form,
|
form,
|
||||||
yield* form.field(["content"]),
|
Form.focusObjectField(form, "content"),
|
||||||
yield* form.field(["completedAt"]),
|
Form.focusObjectField(form, "completedAt"),
|
||||||
] as const
|
] as const
|
||||||
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||||
|
|
||||||
@@ -92,11 +91,14 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
|
|||||||
<Flex direction="row" align="center" gap="2">
|
<Flex direction="row" align="center" gap="2">
|
||||||
<Box flexGrow="1">
|
<Box flexGrow="1">
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
<TextFieldFormInput field={contentField} />
|
<TextFieldFormInput
|
||||||
|
form={contentField}
|
||||||
|
debounce="250 millis"
|
||||||
|
/>
|
||||||
|
|
||||||
<Flex direction="row" justify="center" align="center" gap="2">
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
<TextFieldOptionalFormInput
|
<TextFieldOptionalFormInput
|
||||||
field={completedAtField}
|
form={completedAtField}
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user