Compare commits
17 Commits
6996a14fa4
...
dc945b909b
| Author | SHA1 | Date | |
|---|---|---|---|
| dc945b909b | |||
| 6adb7061f4 | |||
|
|
637aeaa04e | ||
|
|
5070c0706d | ||
|
|
92a13efabc | ||
|
|
1accd657e0 | ||
|
|
943c2aa35d | ||
|
|
9dd7592c45 | ||
|
|
f51b1b04ae | ||
|
|
485278558f | ||
|
|
ceb61ef992 | ||
|
|
4cafcfac6f | ||
|
|
a623e8217c | ||
|
|
4b67552a14 | ||
|
|
6f50cf2989 | ||
|
|
aa243c6493 | ||
|
|
10cec68ee2 |
@@ -9,7 +9,7 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Lint TypeScript
|
- name: Lint TypeScript
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Lint TypeScript
|
- name: Lint TypeScript
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Lint TypeScript
|
- name: Lint TypeScript
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -6,7 +6,7 @@
|
|||||||
"name": "@effect-fc/monorepo",
|
"name": "@effect-fc/monorepo",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.4",
|
"@biomejs/biome": "^2.3.4",
|
||||||
"@effect/language-service": "^0.56.0",
|
"@effect/language-service": "^0.58.0",
|
||||||
"@types/bun": "^1.3.2",
|
"@types/bun": "^1.3.2",
|
||||||
"npm-check-updates": "^19.1.2",
|
"npm-check-updates": "^19.1.2",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
|
|
||||||
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
|
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
|
||||||
|
|
||||||
"@effect/language-service": ["@effect/language-service@0.56.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-gvJaHoeXMHAoA6+Xyj9Vdq52yDCs+ECLbKpHvxHtdJP/C0D9b3JFEfLjdVuw37zoWcYS856um4rgEYHlW2LSEQ=="],
|
"@effect/language-service": ["@effect/language-service@0.58.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-M5T9zEEu6sLuzXOIp+bQ8B1pMcX3A9gyahTTWlv9idr+b2SlZOfydomwgXkod4vlXw7mYhLLcXgCsnHcBUz9rw=="],
|
||||||
|
|
||||||
"@effect/platform": ["@effect/platform@0.93.0", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.0" } }, "sha512-VaIv0duA+Dk2h8XYDPxCLCXGbMyd6hwuHUQt9THL1ZEqv1C3Fypg/Gi2UkzRys6TQsSnC9fJbdpMb7haPURYkQ=="],
|
"@effect/platform": ["@effect/platform@0.93.0", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.0" } }, "sha512-VaIv0duA+Dk2h8XYDPxCLCXGbMyd6hwuHUQt9THL1ZEqv1C3Fypg/Gi2UkzRys6TQsSnC9fJbdpMb7haPURYkQ=="],
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.4",
|
"@biomejs/biome": "^2.3.4",
|
||||||
"@effect/language-service": "^0.56.0",
|
"@effect/language-service": "^0.58.0",
|
||||||
"@types/bun": "^1.3.2",
|
"@types/bun": "^1.3.2",
|
||||||
"npm-check-updates": "^19.1.2",
|
"npm-check-updates": "^19.1.2",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ extends Pipeable.Class() implements ErrorObserver<E> {
|
|||||||
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly pubsub: PubSub.PubSub<Cause.Cause<E>>
|
readonly pubsub: PubSub.PubSub<Cause.Cause<E>>
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.subscribe = pubsub.subscribe
|
this.subscribe = pubsub.subscribe
|
||||||
@@ -36,9 +36,10 @@ class ErrorObserverSupervisorImpl extends Supervisor.AbstractSupervisor<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onEnd<A, E>(_value: Exit.Exit<A, E>): void {
|
onEnd<A, E>(_value: Exit.Exit<A, E>): void {
|
||||||
if (Exit.isFailure(_value))
|
if (Exit.isFailure(_value)) {
|
||||||
Effect.runSync(PubSub.publish(this.pubsub, _value.cause as Cause.Cause<never>))
|
Effect.runSync(PubSub.publish(this.pubsub, _value.cause as Cause.Cause<never>))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Array, Cause, Chunk, 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, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
|
||||||
import type { NoSuchElementException } from "effect/Cause"
|
|
||||||
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 Mutation from "./Mutation.js"
|
||||||
import * as PropertyPath from "./PropertyPath.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"
|
||||||
@@ -12,214 +12,212 @@ 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, out R = never, in out SA = void, in out SE = A, out SR = never, in out SP = never>
|
export interface Form<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||||
extends Pipeable.Pipeable {
|
extends Pipeable.Pipeable {
|
||||||
readonly [FormTypeId]: FormTypeId
|
readonly [FormTypeId]: FormTypeId
|
||||||
|
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>
|
readonly context: Context.Context<Scope.Scope | R>
|
||||||
readonly initialSubmitProgress: SP
|
readonly mutation: Mutation.Mutation<
|
||||||
|
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
||||||
|
MA, ME, MR, MP
|
||||||
|
>
|
||||||
readonly autosubmit: boolean
|
readonly autosubmit: boolean
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>
|
readonly debounce: Option.Option<Duration.DurationInput>
|
||||||
|
|
||||||
readonly fieldCacheRef: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>
|
readonly value: Subscribable.Subscribable<Option.Option<A>>
|
||||||
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>
|
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
||||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>
|
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
|
||||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>
|
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
|
||||||
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
|
|
||||||
readonly submitResultRef: SubscriptionRef.SubscriptionRef<Result.Result<SA, SE, SP>>
|
|
||||||
|
|
||||||
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>
|
readonly canSubmit: Subscribable.Subscribable<boolean>
|
||||||
|
|
||||||
|
field<const P extends PropertyPath.Paths<I>>(
|
||||||
|
path: P
|
||||||
|
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>>
|
||||||
|
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormImpl<in out A, in out I = A, out R = never, in out SA = void, in out SE = A, out SR = never, in out SP = never>
|
export class FormImpl<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||||
extends Pipeable.Class() implements Form<A, I, R, SA, SE, SR, SP> {
|
extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
||||||
readonly [FormTypeId]: FormTypeId = FormTypeId
|
readonly [FormTypeId]: FormTypeId = FormTypeId
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly schema: Schema.Schema<A, I, R>,
|
readonly schema: Schema.Schema<A, I, R>,
|
||||||
readonly onSubmit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>,
|
readonly context: Context.Context<Scope.Scope | R>,
|
||||||
readonly initialSubmitProgress: SP,
|
readonly mutation: Mutation.Mutation<
|
||||||
|
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>],
|
||||||
|
MA, ME, MR, MP
|
||||||
|
>,
|
||||||
readonly autosubmit: boolean,
|
readonly autosubmit: boolean,
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>,
|
readonly debounce: Option.Option<Duration.DurationInput>,
|
||||||
|
|
||||||
readonly fieldCacheRef: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
|
readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||||
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
||||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
|
||||||
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
|
|
||||||
readonly submitResultRef: SubscriptionRef.SubscriptionRef<Result.Result<SA, SE, SP>>,
|
|
||||||
|
|
||||||
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>,
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
|
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
this.canSubmit = Subscribable.map(
|
||||||
|
Subscribable.zipLatestAll(this.value, this.error, this.validationFiber, this.mutation.result),
|
||||||
export namespace make {
|
|
||||||
export interface Options<in out A, in out I, in out R, in out SA = void, in out SE = A, out SR = never, in out SP = never> {
|
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
|
||||||
readonly initialEncodedValue: NoInfer<I>
|
|
||||||
readonly onSubmit: (
|
|
||||||
this: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>,
|
|
||||||
value: NoInfer<A>,
|
|
||||||
) => Effect.Effect<SA, SE, Result.forkEffect.InputContext<SR, NoInfer<SP>>>
|
|
||||||
readonly initialSubmitProgress?: SP
|
|
||||||
readonly autosubmit?: boolean
|
|
||||||
readonly debounce?: Duration.DurationInput
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Success<A, I, R, SA = void, SE = A, SR = never, SP = never> = (
|
|
||||||
Form<A, I, R, SA, SE, Exclude<SR, Result.Progress<any> | Result.Progress<never>>, SP>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <A, I = A, R = never, SA = void, SE = A, SR = never, SP = never>(
|
|
||||||
options: make.Options<A, I, R, SA, SE, SR, SP>
|
|
||||||
): Effect.fn.Return<make.Success<A, I, R, SA, SE, SR, SP>> {
|
|
||||||
const valueRef = yield* SubscriptionRef.make(Option.none<A>())
|
|
||||||
const errorRef = yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>())
|
|
||||||
const validationFiberRef = yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>())
|
|
||||||
const submitResultRef = yield* SubscriptionRef.make<Result.Result<SA, SE, SP>>(Result.initial())
|
|
||||||
|
|
||||||
return new FormImpl(
|
|
||||||
options.schema,
|
|
||||||
options.onSubmit as any,
|
|
||||||
options.initialSubmitProgress as SP,
|
|
||||||
options.autosubmit ?? false,
|
|
||||||
Option.fromNullable(options.debounce),
|
|
||||||
|
|
||||||
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
|
|
||||||
valueRef,
|
|
||||||
yield* SubscriptionRef.make(options.initialEncodedValue),
|
|
||||||
errorRef,
|
|
||||||
validationFiberRef,
|
|
||||||
submitResultRef,
|
|
||||||
|
|
||||||
Subscribable.map(
|
|
||||||
Subscribable.zipLatestAll(valueRef, errorRef, validationFiberRef, submitResultRef),
|
|
||||||
([value, error, validationFiber, submitResult]) => (
|
([value, error, validationFiber, submitResult]) => (
|
||||||
Option.isSome(value) &&
|
Option.isSome(value) &&
|
||||||
Option.isNone(error) &&
|
Option.isNone(error) &&
|
||||||
Option.isNone(validationFiber) &&
|
Option.isNone(validationFiber) &&
|
||||||
!(Result.isRunning(submitResult) || Result.isRefreshing(submitResult))
|
!(Result.isRunning(submitResult) || Result.isRefreshing(submitResult))
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
export const run = <A, I, R, SA, SE, SR, SP>(
|
field<const P extends PropertyPath.Paths<I>>(
|
||||||
self: Form<A, I, R, SA, SE, SR, SP>
|
path: P
|
||||||
): Effect.Effect<void, never, Scope.Scope | R | SR> => Stream.runForEach(
|
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>> {
|
||||||
self.encodedValueRef.changes.pipe(
|
return this.fieldCache.pipe(
|
||||||
Option.isSome(self.debounce) ? Stream.debounce(self.debounce.value) : identity
|
Effect.map(HashMap.get(new FormFieldKey(path))),
|
||||||
|
Effect.flatMap(Option.match({
|
||||||
|
onSome: v => Effect.succeed(v as FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>),
|
||||||
|
onNone: () => Effect.tap(
|
||||||
|
Effect.succeed(makeFormField(this as Form<A, I, R, MA, ME, MR, MP>, path)),
|
||||||
|
v => Ref.update(this.fieldCache, HashMap.set(new FormFieldKey(path), v as FormField<unknown, unknown>)),
|
||||||
),
|
),
|
||||||
|
|
||||||
encodedValue => self.validationFiberRef.pipe(
|
|
||||||
Effect.andThen(Option.match({
|
|
||||||
onSome: Fiber.interrupt,
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
})),
|
})),
|
||||||
Effect.andThen(
|
|
||||||
Effect.forkScoped(Effect.onExit(
|
|
||||||
Schema.decode(self.schema, { errors: "all" })(encodedValue),
|
|
||||||
exit => Effect.andThen(
|
|
||||||
Exit.matchEffect(exit, {
|
|
||||||
onSuccess: v => Effect.andThen(
|
|
||||||
Ref.set(self.valueRef, Option.some(v)),
|
|
||||||
Ref.set(self.errorRef, Option.none()),
|
|
||||||
),
|
|
||||||
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
|
||||||
onSome: e => Ref.set(self.errorRef, Option.some(e)),
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
Ref.set(self.validationFiberRef, Option.none()),
|
|
||||||
),
|
|
||||||
)).pipe(
|
|
||||||
Effect.tap(fiber => Ref.set(self.validationFiberRef, Option.some(fiber))),
|
|
||||||
Effect.andThen(Fiber.join),
|
|
||||||
Effect.andThen(() => self.autosubmit
|
|
||||||
? Effect.asVoid(Effect.forkScoped(submit(self)))
|
|
||||||
: Effect.void
|
|
||||||
),
|
|
||||||
Effect.forkScoped,
|
|
||||||
)
|
)
|
||||||
),
|
}
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const submit = <A, I, R, SA, SE, SR, SP>(
|
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>
|
||||||
self: Form<A, I, R, SA, SE, SR, SP>
|
|
||||||
): Effect.Effect<
|
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
||||||
Option.Option<Result.Result<SA, SE, SP>>,
|
return this.value.pipe(
|
||||||
NoSuchElementException,
|
|
||||||
Scope.Scope | SR
|
|
||||||
> => Effect.whenEffect(
|
|
||||||
self.valueRef.pipe(
|
|
||||||
Effect.andThen(identity),
|
Effect.andThen(identity),
|
||||||
Effect.andThen(value => Result.unsafeForkEffect(
|
Effect.andThen(value => this.submitValue(value)),
|
||||||
self.onSubmit(value),
|
)
|
||||||
{ initialProgress: self.initialSubmitProgress },
|
}
|
||||||
)),
|
submitValue(value: A): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>> {
|
||||||
Effect.andThen(([sub]) => Effect.all([Effect.succeed(sub), sub.get])),
|
return Effect.whenEffect(
|
||||||
Effect.andThen(([sub, initial]) => Stream.runFoldEffect(
|
Effect.tap(
|
||||||
sub.changes,
|
this.mutation.mutate([value, this as any]),
|
||||||
initial,
|
result => Result.isFailure(result)
|
||||||
(_, result) => Effect.as(Ref.set(self.submitResultRef, result), result),
|
|
||||||
)),
|
|
||||||
Effect.tap(result => Result.isFailure(result)
|
|
||||||
? Option.match(
|
? Option.match(
|
||||||
Chunk.findFirst(
|
Chunk.findFirst(
|
||||||
Cause.failures(result.cause as Cause.Cause<ParseResult.ParseError>),
|
Cause.failures(result.cause as Cause.Cause<ParseResult.ParseError>),
|
||||||
e => e._tag === "ParseError",
|
e => e._tag === "ParseError",
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
onSome: e => Ref.set(self.errorRef, Option.some(e)),
|
onSome: e => Ref.set(this.error, Option.some(e)),
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: Effect.void
|
: Effect.void
|
||||||
),
|
),
|
||||||
),
|
this.canSubmit.get,
|
||||||
|
)
|
||||||
self.canSubmitSubscribable.get,
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export namespace service {
|
|
||||||
export interface Options<in out A, in out I, in out R, in out SA = void, in out SE = A, out SR = never, in out SP = never>
|
|
||||||
extends make.Options<A, I, R, SA, SE, SR, SP> {}
|
|
||||||
|
|
||||||
export type Return<A, I, R, SA = void, SE = A, SR = never, SP = never> = Effect.Effect<
|
|
||||||
Form<A, I, R, SA, SE, Exclude<SR, Result.Progress<any> | Result.Progress<never>>, SP>,
|
|
||||||
never,
|
|
||||||
Scope.Scope | R | Exclude<SR, Result.Progress<any> | Result.Progress<never>>
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const service = <A, I = A, R = never, SA = void, SE = A, SR = never, SP = never>(
|
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
|
||||||
options: service.Options<A, I, R, SA, SE, SR, SP>
|
|
||||||
): service.Return<A, I, R, SA, SE, SR, SP> => Effect.tap(
|
export 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>
|
||||||
|
extends Mutation.make.Options<
|
||||||
|
readonly [value: NoInfer<A>, form: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
|
||||||
|
MA, ME, MR, MP
|
||||||
|
> {
|
||||||
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
|
readonly initialEncodedValue: NoInfer<I>
|
||||||
|
readonly 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>(
|
||||||
|
options: make.Options<A, I, R, MA, ME, MR, MP>
|
||||||
|
): Effect.fn.Return<
|
||||||
|
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
||||||
|
> {
|
||||||
|
return new FormImpl(
|
||||||
|
options.schema,
|
||||||
|
yield* Effect.context<Scope.Scope | R>(),
|
||||||
|
yield* Mutation.make(options),
|
||||||
|
options.autosubmit ?? false,
|
||||||
|
Option.fromNullable(options.debounce),
|
||||||
|
|
||||||
|
yield* SubscriptionRef.make(Option.none<A>()),
|
||||||
|
yield* SubscriptionRef.make(options.initialEncodedValue),
|
||||||
|
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()),
|
||||||
|
|
||||||
|
yield* Effect.makeSemaphore(1),
|
||||||
|
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const run = <A, I, R, MA, ME, MR, MP>(
|
||||||
|
self: Form<A, I, R, MA, ME, MR, MP>
|
||||||
|
): Effect.Effect<void> => {
|
||||||
|
const _self = self as FormImpl<A, I, R, MA, ME, MR, MP>
|
||||||
|
return _self.runSemaphore.withPermits(1)(Stream.runForEach(
|
||||||
|
_self.encodedValue.changes.pipe(
|
||||||
|
Option.isSome(_self.debounce) ? Stream.debounce(_self.debounce.value) : identity
|
||||||
|
),
|
||||||
|
|
||||||
|
encodedValue => _self.validationFiber.pipe(
|
||||||
|
Effect.andThen(Option.match({
|
||||||
|
onSome: Fiber.interrupt,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
})),
|
||||||
|
Effect.andThen(
|
||||||
|
Effect.forkScoped(Effect.onExit(
|
||||||
|
Schema.decode(_self.schema, { errors: "all" })(encodedValue),
|
||||||
|
exit => Effect.andThen(
|
||||||
|
Exit.matchEffect(exit, {
|
||||||
|
onSuccess: v => Effect.andThen(
|
||||||
|
Ref.set(_self.value, Option.some(v)),
|
||||||
|
Ref.set(_self.error, Option.none()),
|
||||||
|
),
|
||||||
|
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
||||||
|
onSome: e => Ref.set(_self.error, Option.some(e)),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Ref.set(_self.validationFiber, Option.none()),
|
||||||
|
),
|
||||||
|
)).pipe(
|
||||||
|
Effect.tap(fiber => Ref.set(_self.validationFiber, Option.some(fiber))),
|
||||||
|
Effect.andThen(Fiber.join),
|
||||||
|
Effect.andThen(value => _self.autosubmit
|
||||||
|
? Effect.asVoid(Effect.forkScoped(_self.submitValue(value)))
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
|
Effect.forkScoped,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Effect.provide(_self.context),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace service {
|
||||||
|
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||||
|
extends make.Options<A, I, R, MA, ME, MR, MP> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const service = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
|
||||||
|
options: service.Options<A, I, R, MA, ME, MR, MP>
|
||||||
|
): Effect.Effect<
|
||||||
|
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
||||||
|
> => Effect.tap(
|
||||||
make(options),
|
make(options),
|
||||||
form => Effect.forkScoped(run(form)),
|
form => Effect.forkScoped(run(form)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const field = <A, I, R, SA, SE, SR, SP, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
|
||||||
self: Form<A, I, R, SA, SE, SR, SP>,
|
|
||||||
path: P,
|
|
||||||
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>> => self.fieldCacheRef.pipe(
|
|
||||||
Effect.map(HashMap.get(new FormFieldKey(path))),
|
|
||||||
Effect.flatMap(Option.match({
|
|
||||||
onSome: v => Effect.succeed(v as FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>),
|
|
||||||
onNone: () => Effect.tap(
|
|
||||||
Effect.succeed(makeFormField(self, path)),
|
|
||||||
v => Ref.update(self.fieldCacheRef, HashMap.set(new FormFieldKey(path), v as FormField<unknown, unknown>)),
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField")
|
export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField")
|
||||||
export type FormFieldTypeId = typeof FormFieldTypeId
|
export type FormFieldTypeId = typeof FormFieldTypeId
|
||||||
@@ -228,11 +226,11 @@ export interface FormField<in out A, in out I = A>
|
|||||||
extends Pipeable.Pipeable {
|
extends Pipeable.Pipeable {
|
||||||
readonly [FormFieldTypeId]: FormFieldTypeId
|
readonly [FormFieldTypeId]: FormFieldTypeId
|
||||||
|
|
||||||
readonly valueSubscribable: Subscribable.Subscribable<Option.Option<A>, NoSuchElementException>
|
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
|
||||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>
|
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
||||||
readonly issuesSubscribable: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
||||||
readonly isValidatingSubscribable: Subscribable.Subscribable<boolean>
|
readonly isValidating: Subscribable.Subscribable<boolean>
|
||||||
readonly isSubmittingSubscribable: Subscribable.Subscribable<boolean>
|
readonly isSubmitting: Subscribable.Subscribable<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormFieldImpl<in out A, in out I = A>
|
class FormFieldImpl<in out A, in out I = A>
|
||||||
@@ -240,11 +238,11 @@ extends Pipeable.Class() implements FormField<A, I> {
|
|||||||
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
|
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly valueSubscribable: Subscribable.Subscribable<Option.Option<A>, NoSuchElementException>,
|
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
|
||||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
||||||
readonly issuesSubscribable: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
||||||
readonly isValidatingSubscribable: Subscribable.Subscribable<boolean>,
|
readonly isValidating: Subscribable.Subscribable<boolean>,
|
||||||
readonly isSubmittingSubscribable: Subscribable.Subscribable<boolean>,
|
readonly isSubmitting: Subscribable.Subscribable<boolean>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@@ -268,25 +266,28 @@ class FormFieldKey implements Equal.Equal {
|
|||||||
export const isFormField = (u: unknown): u is FormField<unknown, unknown> => Predicate.hasProperty(u, FormFieldTypeId)
|
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)
|
const isFormFieldKey = (u: unknown): u is FormFieldKey => Predicate.hasProperty(u, FormFieldKeyTypeId)
|
||||||
|
|
||||||
export const makeFormField = <A, I, R, SA, SE, SR, SP, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||||
self: Form<A, I, R, SA, SE, SR, SP>,
|
self: Form<A, I, R, MA, ME, MR, MP>,
|
||||||
path: P,
|
path: P,
|
||||||
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => new FormFieldImpl(
|
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => {
|
||||||
Subscribable.mapEffect(self.valueRef, Option.match({
|
const _self = self as FormImpl<A, I, R, MA, ME, MR, MP>
|
||||||
|
return new FormFieldImpl(
|
||||||
|
Subscribable.mapEffect(_self.value, Option.match({
|
||||||
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
||||||
onNone: () => Option.some(Option.none()),
|
onNone: () => Option.some(Option.none()),
|
||||||
})),
|
})),
|
||||||
SubscriptionSubRef.makeFromPath(self.encodedValueRef, path),
|
SubscriptionSubRef.makeFromPath(_self.encodedValue, path),
|
||||||
Subscribable.mapEffect(self.errorRef, Option.match({
|
Subscribable.mapEffect(_self.error, Option.match({
|
||||||
onSome: flow(
|
onSome: flow(
|
||||||
ParseResult.ArrayFormatter.formatError,
|
ParseResult.ArrayFormatter.formatError,
|
||||||
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
|
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
|
||||||
),
|
),
|
||||||
onNone: () => Effect.succeed([]),
|
onNone: () => Effect.succeed([]),
|
||||||
})),
|
})),
|
||||||
Subscribable.map(self.validationFiberRef, Option.isSome),
|
Subscribable.map(_self.validationFiber, Option.isSome),
|
||||||
Subscribable.map(self.submitResultRef, result => Result.isRunning(result) || Result.isRefreshing(result)),
|
Subscribable.map(_self.mutation.result, result => Result.isRunning(result) || Result.isRefreshing(result)),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export namespace useInput {
|
export namespace useInput {
|
||||||
@@ -303,12 +304,12 @@ export namespace useInput {
|
|||||||
export const useInput = Effect.fnUntraced(function* <A, I>(
|
export const useInput = Effect.fnUntraced(function* <A, I>(
|
||||||
field: FormField<A, I>,
|
field: FormField<A, I>,
|
||||||
options?: useInput.Options,
|
options?: useInput.Options,
|
||||||
): Effect.fn.Return<useInput.Result<I>, NoSuchElementException, Scope.Scope> {
|
): Effect.fn.Return<useInput.Result<I>, Cause.NoSuchElementException, Scope.Scope> {
|
||||||
const internalValueRef = yield* Component.useOnChange(() => Effect.tap(
|
const internalValueRef = yield* Component.useOnChange(() => Effect.tap(
|
||||||
Effect.andThen(field.encodedValueRef, SubscriptionRef.make),
|
Effect.andThen(field.encodedValue, SubscriptionRef.make),
|
||||||
internalValueRef => Effect.forkScoped(Effect.all([
|
internalValueRef => Effect.forkScoped(Effect.all([
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
Stream.drop(field.encodedValueRef, 1),
|
Stream.drop(field.encodedValue, 1),
|
||||||
upstreamEncodedValue => Effect.whenEffect(
|
upstreamEncodedValue => Effect.whenEffect(
|
||||||
Ref.set(internalValueRef, upstreamEncodedValue),
|
Ref.set(internalValueRef, upstreamEncodedValue),
|
||||||
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
||||||
@@ -321,7 +322,7 @@ export const useInput = Effect.fnUntraced(function* <A, I>(
|
|||||||
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.encodedValueRef, internalValue),
|
internalValue => Ref.set(field.encodedValue, internalValue),
|
||||||
),
|
),
|
||||||
], { concurrency: "unbounded" })),
|
], { concurrency: "unbounded" })),
|
||||||
), [field, options?.debounce])
|
), [field, options?.debounce])
|
||||||
@@ -344,10 +345,10 @@ export namespace useOptionalInput {
|
|||||||
export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
||||||
field: FormField<A, Option.Option<I>>,
|
field: FormField<A, Option.Option<I>>,
|
||||||
options: useOptionalInput.Options<I>,
|
options: useOptionalInput.Options<I>,
|
||||||
): Effect.fn.Return<useOptionalInput.Result<I>, NoSuchElementException, Scope.Scope> {
|
): Effect.fn.Return<useOptionalInput.Result<I>, Cause.NoSuchElementException, Scope.Scope> {
|
||||||
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap(
|
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap(
|
||||||
Effect.andThen(
|
Effect.andThen(
|
||||||
field.encodedValueRef,
|
field.encodedValue,
|
||||||
Option.match({
|
Option.match({
|
||||||
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
|
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
|
||||||
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
|
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
|
||||||
@@ -356,7 +357,7 @@ export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
|||||||
|
|
||||||
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([
|
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
Stream.drop(field.encodedValueRef, 1),
|
Stream.drop(field.encodedValue, 1),
|
||||||
|
|
||||||
upstreamEncodedValue => Effect.whenEffect(
|
upstreamEncodedValue => Effect.whenEffect(
|
||||||
Option.match(upstreamEncodedValue, {
|
Option.match(upstreamEncodedValue, {
|
||||||
@@ -384,7 +385,7 @@ export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
|||||||
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.encodedValueRef, enabled ? Option.some(internalValue) : Option.none()),
|
([enabled, internalValue]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||||
),
|
),
|
||||||
], { concurrency: "unbounded" })),
|
], { concurrency: "unbounded" })),
|
||||||
), [field, options.debounce])
|
), [field, options.debounce])
|
||||||
|
|||||||
123
packages/effect-fc/src/Mutation.ts
Normal file
123
packages/effect-fc/src/Mutation.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { type Context, Effect, Equal, type Fiber, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
|
||||||
|
import * as Result from "./Result.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const MutationTypeId: unique symbol = Symbol.for("@effect-fc/Mutation/Mutation")
|
||||||
|
export type MutationTypeId = typeof MutationTypeId
|
||||||
|
|
||||||
|
export interface Mutation<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
|
||||||
|
extends Pipeable.Pipeable {
|
||||||
|
readonly [MutationTypeId]: MutationTypeId
|
||||||
|
|
||||||
|
readonly context: Context.Context<Scope.Scope | R>
|
||||||
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
|
readonly initialProgress: P
|
||||||
|
|
||||||
|
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||||
|
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
|
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
|
||||||
|
mutate(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||||
|
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MutationImpl<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
|
||||||
|
extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
|
||||||
|
readonly [MutationTypeId]: MutationTypeId = MutationTypeId
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly context: Context.Context<Scope.Scope | NoInfer<R>>,
|
||||||
|
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||||
|
readonly initialProgress: P,
|
||||||
|
|
||||||
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||||
|
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
|
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
mutate(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
|
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
||||||
|
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
||||||
|
Effect.andThen(sub => this.watch(sub)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
||||||
|
return Effect.andThen(
|
||||||
|
SubscriptionRef.set(this.latestKey, Option.some(key)),
|
||||||
|
Effect.provide(this.start(key), this.context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
start(key: K): Effect.Effect<
|
||||||
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | R
|
||||||
|
> {
|
||||||
|
return this.result.pipe(
|
||||||
|
Effect.map(previous => Result.isFinal(previous)
|
||||||
|
? previous
|
||||||
|
: undefined
|
||||||
|
),
|
||||||
|
Effect.andThen(previous => Result.unsafeForkEffect(
|
||||||
|
Effect.onExit(this.f(key), () => Effect.andThen(
|
||||||
|
Effect.all([Effect.fiberId, this.fiber]),
|
||||||
|
([currentFiberId, fiber]) => Option.match(fiber, {
|
||||||
|
onSome: v => Equal.equals(currentFiberId, v.id())
|
||||||
|
? SubscriptionRef.set(this.fiber, Option.none())
|
||||||
|
: Effect.void,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
})
|
||||||
|
)),
|
||||||
|
|
||||||
|
{
|
||||||
|
initialProgress: this.initialProgress,
|
||||||
|
previous,
|
||||||
|
} as Result.unsafeForkEffect.Options<A, E, P>,
|
||||||
|
)),
|
||||||
|
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
||||||
|
Effect.map(([sub]) => sub),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
|
return Effect.andThen(
|
||||||
|
sub.get,
|
||||||
|
initial => Stream.runFoldEffect(
|
||||||
|
Stream.filter(sub.changes, Predicate.not(Result.isInitial)),
|
||||||
|
initial,
|
||||||
|
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
||||||
|
),
|
||||||
|
) as Effect.Effect<Result.Final<A, E, P>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMutation = (u: unknown): u is Mutation<unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, MutationTypeId)
|
||||||
|
|
||||||
|
export declare namespace make {
|
||||||
|
export interface Options<K extends readonly any[] = never, A = void, E = never, R = never, P = never> {
|
||||||
|
readonly f: (key: K) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
||||||
|
readonly initialProgress?: P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = Effect.fnUntraced(function* <const K extends readonly any[] = never, A = void, E = never, R = never, P = never>(
|
||||||
|
options: make.Options<K, A, E, R, P>
|
||||||
|
): Effect.fn.Return<
|
||||||
|
Mutation<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
|
||||||
|
> {
|
||||||
|
return new MutationImpl(
|
||||||
|
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||||
|
options.f as any,
|
||||||
|
options.initialProgress as P,
|
||||||
|
|
||||||
|
yield* SubscriptionRef.make(Option.none<K>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
||||||
|
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Effect, Fiber, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
|
import { type Cause, type Context, Effect, Fiber, identity, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
|
||||||
import * as Result from "./Result.js"
|
import * as Result from "./Result.js"
|
||||||
|
|
||||||
|
|
||||||
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
export const QueryTypeId: unique symbol = Symbol.for("@effect-fc/Query/Query")
|
||||||
export type QueryTypeId = typeof QueryTypeId
|
export type QueryTypeId = typeof QueryTypeId
|
||||||
|
|
||||||
export interface Query<in out K extends readonly any[], in out A, in out E = never, out R = never, in out P = never>
|
export interface Query<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
|
||||||
extends Pipeable.Pipeable {
|
extends Pipeable.Pipeable {
|
||||||
readonly [QueryTypeId]: QueryTypeId
|
readonly [QueryTypeId]: QueryTypeId
|
||||||
|
|
||||||
|
readonly context: Context.Context<Scope.Scope | R>
|
||||||
readonly key: Stream.Stream<K>
|
readonly key: Stream.Stream<K>
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>
|
readonly f: (key: K) => Effect.Effect<A, E, R>
|
||||||
readonly initialProgress: P
|
readonly initialProgress: P
|
||||||
@@ -16,13 +17,21 @@ extends Pipeable.Pipeable {
|
|||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
|
||||||
|
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||||
|
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||||
|
readonly refetch: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
||||||
|
readonly refetchSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
||||||
|
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
||||||
|
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueryImpl<in out K extends readonly any[], in out A, in out E = never, out R = never, in out P = never>
|
export class QueryImpl<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
|
||||||
extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
||||||
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
readonly [QueryTypeId]: QueryTypeId = QueryTypeId
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
readonly context: Context.Context<Scope.Scope | NoInfer<R>>,
|
||||||
readonly key: Stream.Stream<K>,
|
readonly key: Stream.Stream<K>,
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||||
readonly initialProgress: P,
|
readonly initialProgress: P,
|
||||||
@@ -30,6 +39,8 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
|||||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||||
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||||
|
|
||||||
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@@ -41,15 +52,71 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
start(key: K): Effect.Effect<
|
fetch(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||||
|
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
||||||
|
Effect.andThen(sub => this.watch(sub)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||||
|
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
get refetch(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(this.latestKey),
|
||||||
|
Effect.andThen(identity),
|
||||||
|
Effect.andThen(key => Effect.provide(this.start(key), this.context)),
|
||||||
|
Effect.andThen(sub => this.watch(sub)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
get refetchSubscribable(): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(this.latestKey),
|
||||||
|
Effect.andThen(identity),
|
||||||
|
Effect.andThen(key => Effect.provide(this.start(key), this.context)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
get refresh(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(this.latestKey),
|
||||||
|
Effect.andThen(identity),
|
||||||
|
Effect.andThen(key => Effect.provide(this.start(key, true), this.context)),
|
||||||
|
Effect.andThen(sub => this.watch(sub)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
get refreshSubscribable(): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException> {
|
||||||
|
return this.interrupt.pipe(
|
||||||
|
Effect.andThen(this.latestKey),
|
||||||
|
Effect.andThen(identity),
|
||||||
|
Effect.andThen(key => Effect.provide(this.start(key, true), this.context)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
start(
|
||||||
|
key: K,
|
||||||
|
refresh?: boolean,
|
||||||
|
): Effect.Effect<
|
||||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | R
|
Scope.Scope | R
|
||||||
> {
|
> {
|
||||||
return Result.unsafeForkEffect(
|
return this.result.pipe(
|
||||||
|
Effect.map(previous => Result.isFinal(previous)
|
||||||
|
? previous
|
||||||
|
: undefined
|
||||||
|
),
|
||||||
|
Effect.andThen(previous => Result.unsafeForkEffect(
|
||||||
Effect.onExit(this.f(key), () => SubscriptionRef.set(this.fiber, Option.none())),
|
Effect.onExit(this.f(key), () => SubscriptionRef.set(this.fiber, Option.none())),
|
||||||
{ initialProgress: this.initialProgress },
|
{
|
||||||
).pipe(
|
initialProgress: this.initialProgress,
|
||||||
|
refresh: refresh && previous,
|
||||||
|
previous,
|
||||||
|
} as Result.unsafeForkEffect.Options<A, E, P>,
|
||||||
|
)),
|
||||||
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
||||||
Effect.map(([sub]) => sub),
|
Effect.map(([sub]) => sub),
|
||||||
)
|
)
|
||||||
@@ -57,15 +124,15 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
): Effect.Effect<Result.Result<A, E, P>> {
|
): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
return Effect.andThen(
|
return Effect.andThen(
|
||||||
sub.get,
|
sub.get,
|
||||||
initial => Stream.runFoldEffect(
|
initial => Stream.runFoldEffect(
|
||||||
sub.changes,
|
Stream.filter(sub.changes, Predicate.not(Result.isInitial)),
|
||||||
initial,
|
initial,
|
||||||
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
||||||
),
|
),
|
||||||
)
|
) as Effect.Effect<Result.Final<A, E, P>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +148,13 @@ export declare namespace make {
|
|||||||
|
|
||||||
export const make = Effect.fnUntraced(function* <K extends readonly any[], A, E = never, R = never, P = never>(
|
export const make = Effect.fnUntraced(function* <K extends readonly any[], A, E = never, R = never, P = never>(
|
||||||
options: make.Options<K, A, E, R, P>
|
options: make.Options<K, A, E, R, P>
|
||||||
): Effect.fn.Return<Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>> {
|
): Effect.fn.Return<
|
||||||
|
Query<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
|
||||||
|
> {
|
||||||
return new QueryImpl(
|
return new QueryImpl(
|
||||||
|
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
|
||||||
options.key,
|
options.key,
|
||||||
options.f as any,
|
options.f as any,
|
||||||
options.initialProgress as P,
|
options.initialProgress as P,
|
||||||
@@ -90,6 +162,8 @@ export const make = Effect.fnUntraced(function* <K extends readonly any[], A, E
|
|||||||
yield* SubscriptionRef.make(Option.none<K>()),
|
yield* SubscriptionRef.make(Option.none<K>()),
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
||||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
||||||
|
|
||||||
|
yield* Effect.makeSemaphore(1),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -106,12 +180,13 @@ export const service = <K extends readonly any[], A, E = never, R = never, P = n
|
|||||||
|
|
||||||
export const run = <K extends readonly any[], A, E, R, P>(
|
export const run = <K extends readonly any[], A, E, R, P>(
|
||||||
self: Query<K, A, E, R, P>
|
self: Query<K, A, E, R, P>
|
||||||
): Effect.Effect<void, never, Scope.Scope | R> => Stream.runForEach(self.key, key =>
|
): Effect.Effect<void> => {
|
||||||
(self as QueryImpl<K, A, E, R, P>).interrupt.pipe(
|
const _self = self as QueryImpl<K, A, E, R, P>
|
||||||
Effect.andThen(SubscriptionRef.set((self as QueryImpl<K, A, E, R, P>).latestKey, Option.some(key))),
|
return Stream.runForEach(_self.key, key => _self.interrupt.pipe(
|
||||||
Effect.andThen((self as QueryImpl<K, A, E, R, P>).start(key)),
|
Effect.andThen(SubscriptionRef.set(_self.latestKey, Option.some(key))),
|
||||||
Effect.andThen(sub => Effect.forkScoped(
|
Effect.andThen(_self.start(key)),
|
||||||
(self as QueryImpl<K, A, E, R, P>).watch(sub)
|
Effect.andThen(sub => Effect.forkScoped(_self.watch(sub))),
|
||||||
)),
|
Effect.provide(_self.context),
|
||||||
)
|
_self.runSemaphore.withPermits(1),
|
||||||
)
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export type ResultTypeId = typeof ResultTypeId
|
|||||||
export type Result<A, E = never, P = never> = (
|
export type Result<A, E = never, P = never> = (
|
||||||
| Initial
|
| Initial
|
||||||
| Running<P>
|
| Running<P>
|
||||||
|
| Final<A, E, P>
|
||||||
|
)
|
||||||
|
|
||||||
|
export type Final<A, E = never, P = never> = (
|
||||||
| Success<A>
|
| Success<A>
|
||||||
| (Success<A> & Refreshing<P>)
|
| (Success<A> & Refreshing<P>)
|
||||||
| Failure<A, E>
|
| Failure<A, E>
|
||||||
@@ -96,6 +100,7 @@ const ResultPrototype = Object.freeze({
|
|||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
export const isFinal = (u: unknown): u is Final<unknown, unknown, unknown> => isResult(u) && (isSuccess(u) || isFailure(u))
|
||||||
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
|
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
|
||||||
export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u._tag === "Running"
|
export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u._tag === "Running"
|
||||||
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
|
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
|
||||||
@@ -111,25 +116,27 @@ export const succeed = <A>(value: A): Success<A> => Object.setPrototypeOf({ _tag
|
|||||||
|
|
||||||
export const fail = <E, A = never>(
|
export const fail = <E, A = never>(
|
||||||
cause: Cause.Cause<E>,
|
cause: Cause.Cause<E>,
|
||||||
previousSuccess?: Success<A>,
|
previousSuccess?: Success<NoInfer<A>>,
|
||||||
): Failure<A, E> => Object.setPrototypeOf({
|
): Failure<A, E> => Object.setPrototypeOf({
|
||||||
_tag: "Failure",
|
_tag: "Failure",
|
||||||
cause,
|
cause,
|
||||||
previousSuccess: Option.fromNullable(previousSuccess),
|
previousSuccess: Option.fromNullable(previousSuccess),
|
||||||
}, ResultPrototype)
|
}, ResultPrototype)
|
||||||
|
|
||||||
export const refreshing = <R extends Success<any> | Failure<any, any>, P = never>(
|
export const refreshing = <R extends Success<any> | Failure<any, any>, P = never>(
|
||||||
result: R,
|
result: R,
|
||||||
progress?: P,
|
progress?: P,
|
||||||
): Omit<R, keyof Refreshing<Result.Progress<R>>> & Refreshing<P> => Object.setPrototypeOf(
|
): Omit<R, keyof Refreshing<Result.Progress<R>>> & Refreshing<P> => Object.setPrototypeOf(
|
||||||
Object.assign({}, result, { progress }),
|
Object.assign({}, result, { refreshing: true, progress }),
|
||||||
Object.getPrototypeOf(result),
|
Object.getPrototypeOf(result),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const fromExit = <A, E>(
|
export const fromExit = <A, E>(
|
||||||
exit: Exit.Exit<A, E>
|
exit: Exit.Exit<A, E>,
|
||||||
|
previousSuccess?: Success<NoInfer<A>>,
|
||||||
): Success<A> | Failure<A, E> => exit._tag === "Success"
|
): Success<A> | Failure<A, E> => exit._tag === "Success"
|
||||||
? succeed(exit.value)
|
? succeed(exit.value)
|
||||||
: fail(exit.cause)
|
: fail(exit.cause, previousSuccess)
|
||||||
|
|
||||||
export const toExit = <A, E, P>(
|
export const toExit = <A, E, P>(
|
||||||
self: Result<A, E, P>
|
self: Result<A, E, P>
|
||||||
@@ -192,14 +199,23 @@ export const makeProgressLayer = <A, E, P = never>(): Layer.Layer<
|
|||||||
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<A, E, R, P> = Exclude<R, State<A, E, P> | Progress<P> | Progress<never>>
|
||||||
|
|
||||||
export interface Options<P> {
|
export type Options<A, E, P> = {
|
||||||
readonly initialProgress?: P
|
readonly initialProgress?: P
|
||||||
|
readonly previous?: Final<A, E, P>
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
readonly refresh: true
|
||||||
|
readonly previous: Final<A, E, P>
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
readonly refresh?: false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unsafeForkEffect = <A, E, R, P = never>(
|
export const unsafeForkEffect = <A, E, R, P = never>(
|
||||||
effect: Effect.Effect<A, E, R>,
|
effect: Effect.Effect<A, E, R>,
|
||||||
options?: unsafeForkEffect.Options<P>,
|
options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||||
): 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,
|
||||||
@@ -208,10 +224,13 @@ export const unsafeForkEffect = <A, E, R, P = never>(
|
|||||||
Effect.bind("ref", () => Ref.make(initial<A, E, P>())),
|
Effect.bind("ref", () => Ref.make(initial<A, E, P>())),
|
||||||
Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()),
|
Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()),
|
||||||
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
|
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
|
||||||
Effect.andThen(state => state.set(running(options?.initialProgress)).pipe(
|
Effect.andThen(state => state.set(options?.refresh
|
||||||
|
? refreshing(options.previous, options?.initialProgress) as Result<A, E, P>
|
||||||
|
: running(options?.initialProgress)
|
||||||
|
).pipe(
|
||||||
Effect.andThen(effect),
|
Effect.andThen(effect),
|
||||||
Effect.onExit(exit => Effect.andThen(
|
Effect.onExit(exit => Effect.andThen(
|
||||||
state.set(fromExit(exit)),
|
state.set(fromExit(exit, (options?.previous && isSuccess(options.previous)) ? options.previous : undefined)),
|
||||||
Effect.forkScoped(PubSub.shutdown(pubsub)),
|
Effect.forkScoped(PubSub.shutdown(pubsub)),
|
||||||
)),
|
)),
|
||||||
)),
|
)),
|
||||||
@@ -242,13 +261,13 @@ export const unsafeForkEffect = <A, E, R, P = never>(
|
|||||||
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<A, E, R, P> = unsafeForkEffect.OutputContext<A, E, R, P>
|
||||||
export interface Options<P> extends unsafeForkEffect.Options<P> {}
|
export type Options<A, E, P> = unsafeForkEffect.Options<A, E, P>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const forkEffect: {
|
export const forkEffect: {
|
||||||
<A, E, R, P = never>(
|
<A, E, R, P = never>(
|
||||||
effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>,
|
effect: Effect.Effect<A, E, forkEffect.InputContext<R, NoInfer<P>>>,
|
||||||
options?: forkEffect.Options<P>,
|
options?: forkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
|
||||||
): 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,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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 Memoized from "./Memoized.js"
|
export * as Memoized from "./Memoized.js"
|
||||||
|
export * as Mutation from "./Mutation.js"
|
||||||
export * as PropertyPath from "./PropertyPath.js"
|
export * as PropertyPath from "./PropertyPath.js"
|
||||||
export * as PubSub from "./PubSub.js"
|
export * as PubSub from "./PubSub.js"
|
||||||
export * as Query from "./Query.js"
|
export * as Query from "./Query.js"
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInp
|
|||||||
: { optional: false, ...yield* Form.useInput(props.field, props) }
|
: { optional: false, ...yield* Form.useInput(props.field, props) }
|
||||||
|
|
||||||
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
|
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
|
||||||
props.field.issuesSubscribable,
|
props.field.issues,
|
||||||
props.field.isValidatingSubscribable,
|
props.field.isValidating,
|
||||||
props.field.isSubmittingSubscribable,
|
props.field.isSubmitting,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
|||||||
),
|
),
|
||||||
|
|
||||||
initialEncodedValue: { email: "", password: "", birth: Option.none() },
|
initialEncodedValue: { email: "", password: "", birth: Option.none() },
|
||||||
onSubmit: Effect.fnUntraced(function*(v) {
|
f: Effect.fnUntraced(function*([value]) {
|
||||||
yield* Effect.sleep("500 millis")
|
yield* Effect.sleep("500 millis")
|
||||||
return yield* Schema.decode(RegisterFormSubmitSchema)(v)
|
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
|
||||||
}),
|
}),
|
||||||
debounce: "500 millis",
|
debounce: "500 millis",
|
||||||
})
|
})
|
||||||
@@ -65,8 +65,8 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
|||||||
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
|
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
|
||||||
const form = yield* RegisterForm
|
const form = yield* RegisterForm
|
||||||
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
||||||
form.canSubmitSubscribable,
|
form.canSubmit,
|
||||||
form.submitResultRef,
|
form.mutation.result,
|
||||||
])
|
])
|
||||||
|
|
||||||
const runPromise = yield* Component.useRunPromise()
|
const runPromise = yield* Component.useRunPromise()
|
||||||
@@ -82,21 +82,21 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
|
|||||||
<Container width="300">
|
<Container width="300">
|
||||||
<form onSubmit={e => {
|
<form onSubmit={e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
void runPromise(Form.submit(form))
|
void runPromise(form.submit)
|
||||||
}}>
|
}}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<TextFieldFormInputFC
|
<TextFieldFormInputFC
|
||||||
field={yield* Form.field(form, ["email"])}
|
field={yield* form.field(["email"])}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextFieldFormInputFC
|
<TextFieldFormInputFC
|
||||||
field={yield* Form.field(form, ["password"])}
|
field={yield* form.field(["password"])}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextFieldFormInputFC
|
<TextFieldFormInputFC
|
||||||
optional
|
optional
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
field={yield* Form.field(form, ["birth"])}
|
field={yield* form.field(["birth"])}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { HttpClient, type HttpClientError } from "@effect/platform"
|
import { HttpClient, type HttpClientError } from "@effect/platform"
|
||||||
import { Container, Heading, Slider, Text } from "@radix-ui/themes"
|
import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
||||||
import { Component, ErrorObserver, Query, Subscribable, SubscriptionRef } from "effect-fc"
|
import { Component, ErrorObserver, Mutation, Query, Result, Subscribable, SubscriptionRef } from "effect-fc"
|
||||||
import { runtime } from "@/runtime"
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +14,9 @@ const Post = Schema.Struct({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const ResultView = Component.makeUntraced("Result")(function*() {
|
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||||
const [idRef, query] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
const runPromise = yield* Component.useRunPromise()
|
||||||
|
|
||||||
|
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
const idRef = yield* SubscriptionRef.make(1)
|
const idRef = yield* SubscriptionRef.make(1)
|
||||||
const key = Stream.zipLatest(Stream.make("posts" as const), idRef.changes)
|
const key = Stream.zipLatest(Stream.make("posts" as const), idRef.changes)
|
||||||
|
|
||||||
@@ -28,11 +30,20 @@ const ResultView = Component.makeUntraced("Result")(function*() {
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
return [idRef, query] as const
|
const mutation = yield* Mutation.make({
|
||||||
|
f: ([id]: readonly [id: number]) => HttpClient.HttpClient.pipe(
|
||||||
|
Effect.tap(Effect.sleep("500 millis")),
|
||||||
|
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||||
|
Effect.andThen(response => response.json),
|
||||||
|
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
return [idRef, query, mutation] as const
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const [id, setId] = yield* SubscriptionRef.useSubscriptionRefState(idRef)
|
const [id, setId] = yield* SubscriptionRef.useSubscriptionRefState(idRef)
|
||||||
const [result] = yield* Subscribable.useSubscribables([query.result])
|
const [queryResult, mutationResult] = yield* Subscribable.useSubscribables([query.result, mutation.result])
|
||||||
|
|
||||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||||
Effect.andThen(observer => observer.subscribe),
|
Effect.andThen(observer => observer.subscribe),
|
||||||
@@ -51,22 +62,51 @@ const ResultView = Component.makeUntraced("Result")(function*() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
<Flex direction="column" align="center" gap="2">
|
||||||
<Slider
|
<Slider
|
||||||
value={[id]}
|
value={[id]}
|
||||||
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
|
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{Match.value(result).pipe(
|
<div>
|
||||||
|
{Match.value(queryResult).pipe(
|
||||||
Match.tag("Running", () => <Text>Loading...</Text>),
|
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||||
Match.tag("Success", result => <>
|
Match.tag("Success", result => <>
|
||||||
<Heading>{result.value.title}</Heading>
|
<Heading>{result.value.title}</Heading>
|
||||||
<Text>{result.value.body}</Text>
|
<Text>{result.value.body}</Text>
|
||||||
|
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
|
||||||
</>),
|
</>),
|
||||||
Match.tag("Failure", result =>
|
Match.tag("Failure", result =>
|
||||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||||
),
|
),
|
||||||
Match.orElse(() => <></>),
|
Match.orElse(() => <></>),
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Flex direction="row" justify="center" align="center" gap="1">
|
||||||
|
<Button onClick={() => runPromise(query.refresh)}>Refresh</Button>
|
||||||
|
<Button onClick={() => runPromise(query.refetch)}>Refetch</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{Match.value(mutationResult).pipe(
|
||||||
|
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||||
|
Match.tag("Success", result => <>
|
||||||
|
<Heading>{result.value.title}</Heading>
|
||||||
|
<Text>{result.value.body}</Text>
|
||||||
|
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
|
||||||
|
</>),
|
||||||
|
Match.tag("Failure", result =>
|
||||||
|
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||||
|
),
|
||||||
|
Match.orElse(() => <></>),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Flex direction="row" justify="center" align="center" gap="1">
|
||||||
|
<Button onClick={() => runPromise(Effect.andThen(idRef, id => mutation.mutate([id])))}>Mutate</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,17 +54,15 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
|
|||||||
Match.exhaustive,
|
Match.exhaustive,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onSubmit: function(todo) {
|
f: ([todo, form]) => Match.value(props).pipe(
|
||||||
return Match.value(props).pipe(
|
|
||||||
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(this.encodedValueRef, v)),
|
Effect.andThen(v => Ref.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",
|
debounce: "250 millis",
|
||||||
})
|
})
|
||||||
@@ -72,15 +70,15 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
|
|||||||
return [
|
return [
|
||||||
indexRef,
|
indexRef,
|
||||||
form,
|
form,
|
||||||
yield* Form.field(form, ["content"]),
|
yield* form.field(["content"]),
|
||||||
yield* Form.field(form, ["completedAt"]),
|
yield* form.field(["completedAt"]),
|
||||||
] as const
|
] as const
|
||||||
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||||
|
|
||||||
const [index, size, canSubmit] = yield* Subscribable.useSubscribables([
|
const [index, size, canSubmit] = yield* Subscribable.useSubscribables([
|
||||||
indexRef,
|
indexRef,
|
||||||
state.sizeSubscribable,
|
state.sizeSubscribable,
|
||||||
form.canSubmitSubscribable,
|
form.canSubmit,
|
||||||
])
|
])
|
||||||
|
|
||||||
const runSync = yield* Component.useRunSync()
|
const runSync = yield* Component.useRunSync()
|
||||||
@@ -103,7 +101,7 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{props._tag === "new" &&
|
{props._tag === "new" &&
|
||||||
<Button disabled={!canSubmit} onClick={() => void runPromise(Form.submit(form))}>
|
<Button disabled={!canSubmit} onClick={() => void runPromise(form.submit)}>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user