0.2.6 #49

Merged
Thilawyn merged 48 commits from next into master 2026-05-04 02:10:53 +02:00
6 changed files with 234 additions and 48 deletions
Showing only changes of commit dbf5d00590 - Show all commits

0
.codex Normal file
View File

View File

@@ -20,8 +20,8 @@ extends Pipeable.Pipeable {
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>
readonly canCommit: Subscribable.Subscribable<boolean, never, never>
readonly isCommitting: 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>
@@ -34,23 +34,25 @@ extends Pipeable.Class() implements Form<P, A, I, ER, EW> {
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>,
readonly canCommit: Subscribable.Subscribable<boolean, never, never>,
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>,
) {
super()
}
}
export const RootFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/RootForm")
export type RootFormTypeId = typeof RootFormTypeId
export const SubmittableFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/SubmittableForm")
export type SubmittableFormTypeId = typeof SubmittableFormTypeId
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>
export interface SubmittableForm<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 [SubmittableFormTypeId]: SubmittableFormTypeId
readonly schema: Schema.Schema<A, I, R>
readonly context: Context.Context<Scope.Scope | R>
readonly mutation: Mutation.Mutation<
readonly [value: A, form: RootForm<A, I, R, unknown, unknown, unknown>],
readonly [value: A, form: SubmittableForm<A, I, R, unknown, unknown, unknown>],
MA, ME, MR, MP
>
readonly autosubmit: boolean
@@ -61,10 +63,10 @@ extends Form<readonly [], A, I, never, never> {
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
}
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 RootForm<A, I, R, MA, ME, MR, MP> {
export class SubmittableFormImpl<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 SubmittableForm<A, I, R, MA, ME, MR, MP> {
readonly [FormTypeId]: FormTypeId = FormTypeId
readonly [RootFormTypeId]: RootFormTypeId = RootFormTypeId
readonly [SubmittableFormTypeId]: SubmittableFormTypeId = SubmittableFormTypeId
readonly path = [] as const
@@ -72,7 +74,7 @@ extends Pipeable.Class() implements RootForm<A, I, R, MA, ME, MR, MP> {
readonly schema: Schema.Schema<A, I, R>,
readonly context: Context.Context<Scope.Scope | R>,
readonly mutation: Mutation.Mutation<
readonly [value: A, form: RootForm<A, I, R, unknown, unknown, unknown>],
readonly [value: A, form: SubmittableForm<A, I, R, unknown, unknown, unknown>],
MA, ME, MR, MP
>,
readonly autosubmit: boolean,
@@ -83,8 +85,8 @@ extends Pipeable.Class() implements RootForm<A, I, R, MA, ME, MR, MP> {
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 canCommit: Subscribable.Subscribable<boolean, never, never>,
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>,
readonly runSemaphore: Effect.Semaphore,
) {
@@ -161,20 +163,141 @@ extends Pipeable.Class() implements RootForm<A, I, R, MA, ME, MR, MP> {
)
: Effect.void
),
this.canSubmit.get,
this.canCommit.get,
)
}
}
export const SynchronizedFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/SynchronizedForm")
export type SynchronizedFormTypeId = typeof SynchronizedFormTypeId
export interface SynchronizedForm<
in out A,
in out I = A,
in out R = never,
in out TER = never,
in out TEW = never,
in out TRR = never,
in out TRW = never,
> extends Form<readonly [], A, I, never, never> {
readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId
readonly schema: Schema.Schema<A, I, R>
readonly context: Context.Context<Scope.Scope | R | TRR | TRW>
readonly target: Lens.Lens<A, TER, TEW, TRR, TRW>
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never>
readonly run: Effect.Effect<void, ParseResult.ParseError | TER | TEW>
}
export class SynchronizedFormImpl<
in out A,
in out I = A,
in out R = never,
in out TER = never,
in out TEW = never,
in out TRR = never,
in out TRW = never,
> extends Pipeable.Class() implements SynchronizedForm<A, I, R, TER, TEW, TRR, TRW> {
readonly [FormTypeId]: FormTypeId = FormTypeId
readonly [SynchronizedFormTypeId]: SynchronizedFormTypeId = SynchronizedFormTypeId
readonly path = [] as const
constructor(
readonly schema: Schema.Schema<A, I, R>,
readonly context: Context.Context<Scope.Scope | R | TRR | TRW>,
readonly target: Lens.Lens<A, TER, TEW, TRR, TRW>,
readonly value: Lens.Lens<Option.Option<A>, never, never, never, never>,
readonly encodedValue: Lens.Lens<I, never, never, never, never>,
readonly issues: Lens.Lens<readonly ParseResult.ArrayFormatterIssue[], never, never, never, never>,
readonly validationFiber: Lens.Lens<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never, never, never>,
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
readonly canCommit: Subscribable.Subscribable<boolean, never, never>,
readonly isCommitting: Subscribable.Subscribable<boolean, never, never>,
readonly runSemaphore: Effect.Semaphore,
) {
super()
}
get run(): Effect.Effect<void, ParseResult.ParseError | TER | TEW> {
return this.runSemaphore.withPermits(1)(Effect.provide(
Effect.all([
Stream.runForEach(
this.encodedValue.changes,
encodedValue => Lens.get(this.validationFiber).pipe(
Effect.andThen(Option.match({
onSome: Fiber.interrupt,
onNone: () => Effect.void,
})),
Effect.andThen(
Effect.forkScoped(Effect.onExit(
Schema.decode(this.schema, { errors: "all" })(encodedValue),
exit => Effect.andThen(
Exit.matchEffect(exit, {
onSuccess: v => Lens.set(this.value, Option.some(v)).pipe(
Effect.andThen(Lens.set(this.issues, Array.empty())),
),
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
onSome: e => Effect.flatMap(
ParseResult.ArrayFormatter.formatError(e),
v => Lens.set(this.issues, v),
),
onNone: () => Effect.void,
}),
}),
Lens.set(this.validationFiber, Option.none()),
),
)).pipe(
Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))),
Effect.andThen(Fiber.join),
Effect.tap(value => Lens.set(this.target, value)),
Effect.forkScoped,
)
),
),
),
Stream.runForEach(
Stream.drop(this.target.changes, 1),
targetValue => Schema.encode(this.schema, { errors: "all" })(targetValue).pipe(
Effect.flatMap(encodedValue => Effect.whenEffect(
Lens.set(this.encodedValue, encodedValue),
Effect.map(
Lens.get(this.encodedValue),
currentEncodedValue => !Equal.equals(encodedValue, currentEncodedValue),
),
)),
),
),
], { concurrency: "unbounded", discard: true }),
this.context,
))
}
}
export const isForm = (u: unknown): u is Form<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 const isSubmittableForm = (u: unknown): u is SubmittableForm<unknown, unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, SubmittableFormTypeId)
export const isSynchronizedForm = (u: unknown): u is SynchronizedForm<unknown, unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, SynchronizedFormTypeId)
const falseSubscribable: Subscribable.Subscribable<boolean, never, never> = Subscribable.make({
get: Effect.succeed(false),
changes: Stream.make(false),
})
export declare namespace make {
export declare namespace makeSubmittable {
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: RootForm<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
readonly [value: NoInfer<A>, form: SubmittableForm<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
MA, ME, MR, MP
> {
readonly schema: Schema.Schema<A, I, R>
@@ -183,10 +306,10 @@ export declare namespace make {
}
}
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>
export const makeSubmittable = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
options: makeSubmittable.Options<A, I, R, MA, ME, MR, MP>
): Effect.fn.Return<
RootForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>,
SubmittableForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>,
never,
Scope.Scope | R | Result.forkEffect.OutputContext<MR, MP>
> {
@@ -195,7 +318,7 @@ export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void,
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(
return new SubmittableFormImpl(
options.schema,
yield* Effect.context<Scope.Scope | R>(),
mutation,
@@ -222,19 +345,82 @@ export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void,
)
})
export declare namespace service {
export declare namespace serviceSubmittable {
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> {}
extends makeSubmittable.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>
export const serviceSubmittable = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
options: serviceSubmittable.Options<A, I, R, MA, ME, MR, MP>
): Effect.Effect<
RootForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>,
SubmittableForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>,
never,
Scope.Scope | R | Result.forkEffect.OutputContext<MR, MP>
> => Effect.tap(
make(options),
makeSubmittable(options),
form => Effect.forkScoped(form.run),
)
export declare namespace makeSynchronized {
export interface Options<in out A, in out I = A, in out R = never, in out TER = never, in out TEW = never, in out TRR = never, in out TRW = never> {
readonly schema: Schema.Schema<A, I, R>
readonly target: Lens.Lens<A, TER, TEW, TRR, TRW>
readonly initialEncodedValue: NoInfer<I>
}
}
export const makeSynchronized = Effect.fnUntraced(function* <A, I = A, R = never, TER = never, TEW = never, TRR = never, TRW = never>(
options: makeSynchronized.Options<A, I, R, TER, TEW, TRR, TRW>
): Effect.fn.Return<
SynchronizedForm<A, I, R, TER, TEW, TRR, TRW>,
ParseResult.ParseError | TER,
Scope.Scope | R | TRR | TRW
> {
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>>()))
const initialEncodedValue = yield* Lens.get(options.target).pipe(
Effect.flatMap(Schema.encode(options.schema, { errors: "all" })),
)
return new SynchronizedFormImpl(
options.schema,
yield* Effect.context<Scope.Scope | R | TRR | TRW>(),
options.target,
valueLens,
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(initialEncodedValue)),
issuesLens,
validationFiberLens,
Subscribable.map(validationFiberLens, Option.isSome),
Subscribable.map(
Subscribable.zipLatestAll(valueLens, issuesLens, validationFiberLens),
([value, issues, validationFiber]) => (
Option.isSome(value) &&
Array.isEmptyReadonlyArray(issues) &&
Option.isNone(validationFiber)
),
),
falseSubscribable,
yield* Effect.makeSemaphore(1),
)
})
export declare namespace serviceSynchronized {
export interface Options<in out A, in out I = A, in out R = never, in out TER = never, in out TEW = never, in out TRR = never, in out TRW = never>
extends makeSynchronized.Options<A, I, R, TER, TEW, TRR, TRW> {}
}
export const serviceSynchronized = <A, I = A, R = never, TER = never, TEW = never, TRR = never, TRW = never>(
options: serviceSynchronized.Options<A, I, R, TER, TEW, TRR, TRW>
): Effect.Effect<
SynchronizedForm<A, I, R, TER, TEW, TRR, TRW>,
ParseResult.ParseError | TER,
Scope.Scope | R | TRR | TRW
> => Effect.tap(
makeSynchronized(options),
form => Effect.forkScoped(form.run),
)
@@ -267,8 +453,8 @@ export const focusObjectField: {
Lens.focusObjectField(form.encodedValue, key),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canSubmit,
form.isSubmitting,
form.canCommit,
form.isCommitting,
)
})
@@ -293,8 +479,8 @@ export const focusArrayAt: {
Lens.focusArrayAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canSubmit,
form.isSubmitting,
form.canCommit,
form.isCommitting,
)
})
@@ -319,8 +505,8 @@ export const focusTupleAt: {
Lens.focusTupleAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canSubmit,
form.isSubmitting,
form.canCommit,
form.isCommitting,
)
})
@@ -345,8 +531,8 @@ export const focusChunkAt: {
Lens.focusChunkAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canSubmit,
form.isSubmitting,
form.canCommit,
form.isCommitting,
)
})

View File

@@ -13,10 +13,10 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi
props: TextFieldFormInputView.Props
) {
const input = yield* Form.useInput(props.form, props)
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
const [issues, isValidating, isCommitting] = yield* Subscribable.useSubscribables([
props.form.issues,
props.form.isValidating,
props.form.isSubmitting,
props.form.isCommitting,
])
return (
@@ -24,7 +24,7 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi
<TextField.Root
value={input.value}
onChange={e => input.setValue(e.target.value)}
disabled={isSubmitting}
disabled={isCommitting}
{...Struct.omit(props, "form")}
>
{isValidating &&

View File

@@ -13,10 +13,10 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt
props: TextFieldOptionalFormInputView.Props
) {
const input = yield* Form.useOptionalInput(props.form, props)
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
const [issues, isValidating, isCommitting] = yield* Subscribable.useSubscribables([
props.form.issues,
props.form.isValidating,
props.form.isSubmitting,
props.form.isCommitting,
])
return (
@@ -24,7 +24,7 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt
<TextField.Root
value={input.value}
onChange={e => input.setValue(e.target.value)}
disabled={!input.enabled || isSubmitting}
disabled={!input.enabled || isCommitting}
{...Struct.omit(props, "form", "defaultValue")}
>
<TextField.Slot side="left">

View File

@@ -73,8 +73,8 @@ class RegisterFormService extends Effect.Service<RegisterFormService>()("Registe
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
const form = yield* RegisterFormService
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
form.form.canSubmit,
const [canCommit, submitResult] = yield* Subscribable.useSubscribables([
form.form.canCommit,
form.form.mutation.result,
])
@@ -111,7 +111,7 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
defaultValue=""
/>
<Button disabled={!canSubmit}>Submit</Button>
<Button disabled={!canCommit}>Submit</Button>
</Flex>
</form>

View File

@@ -75,10 +75,10 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
] as const
}), [props._tag, props._tag === "edit" ? props.id : undefined])
const [index, size, canSubmit] = yield* Subscribable.useSubscribables([
const [index, size, canCommit] = yield* Subscribable.useSubscribables([
indexRef,
state.sizeSubscribable,
form.canSubmit,
form.canCommit,
])
const runSync = yield* Component.useRunSync()
@@ -104,7 +104,7 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
/>
{props._tag === "new" &&
<Button disabled={!canSubmit} onClick={() => void runPromise(form.submit)}>
<Button disabled={!canCommit} onClick={() => void runPromise(form.submit)}>
Add
</Button>
}