Working mutation
All checks were successful
Lint / lint (push) Successful in 13s

This commit is contained in:
Julien Valverdé
2025-11-27 22:21:36 +01:00
parent 485278558f
commit f51b1b04ae
4 changed files with 90 additions and 38 deletions

View File

@@ -17,7 +17,7 @@ extends Pipeable.Pipeable {
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.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>>>
}
@@ -37,13 +37,12 @@ extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
super()
}
mutate(key: K): Effect.Effect<Result.Result<A, E, P>> {
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)),
@@ -57,7 +56,7 @@ extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
Scope.Scope | R
> {
return this.result.pipe(
Effect.map(previous => (Result.isSuccess(previous) || Result.isFailure(previous))
Effect.map(previous => Result.isFinal(previous)
? previous
: undefined
),
@@ -84,7 +83,7 @@ extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
watch(
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(
sub.get,
initial => Stream.runFoldEffect(
@@ -92,20 +91,20 @@ extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
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[], A, E = never, R = never, P = never> {
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
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* <K extends readonly any[], A, E = never, R = never, P = never>(
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>,

View File

@@ -18,9 +18,12 @@ extends Pipeable.Pipeable {
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
fetch(key: K): Effect.Effect<Result.Result<A, E, P>>
readonly refetch: Effect.Effect<Result.Result<A, E, P>, Cause.NoSuchElementException>
readonly refresh: Effect.Effect<Result.Result<A, E, P>, Cause.NoSuchElementException>
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
readonly refetch: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
readonly refetchSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
}
export class QueryImpl<in out K extends readonly any[], in out A, in out E = never, in out R = never, in out P = never>
@@ -47,15 +50,20 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
}))
}
fetch(key: K): Effect.Effect<Result.Result<A, E, P>> {
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)),
)
}
get refetch(): Effect.Effect<Result.Result<A, E, P>, Cause.NoSuchElementException> {
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),
@@ -63,8 +71,14 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
Effect.andThen(sub => this.watch(sub)),
)
}
get refresh(): Effect.Effect<Result.Result<A, E, P>, Cause.NoSuchElementException> {
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),
@@ -72,6 +86,13 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
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,
@@ -82,7 +103,7 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
Scope.Scope | R
> {
return this.result.pipe(
Effect.map(previous => (Result.isSuccess(previous) || Result.isFailure(previous))
Effect.map(previous => Result.isFinal(previous)
? previous
: undefined
),
@@ -101,7 +122,7 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
watch(
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(
sub.get,
initial => Stream.runFoldEffect(
@@ -109,7 +130,7 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
initial,
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
),
)
) as Effect.Effect<Result.Final<A, E, P>>
}
}

View File

@@ -100,6 +100,7 @@ const ResultPrototype = Object.freeze({
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
export const isFinal = (u: unknown): u is Final<unknown, unknown, unknown> => isResult(u) && (isSuccess(u) || isFailure(u))
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u._tag === "Running"
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
@@ -121,6 +122,7 @@ export const fail = <E, A = never>(
cause,
previousSuccess: Option.fromNullable(previousSuccess),
}, ResultPrototype)
export const refreshing = <R extends Success<any> | Failure<any, any>, P = never>(
result: R,
progress?: P,
@@ -199,11 +201,11 @@ export namespace unsafeForkEffect {
export type Options<A, E, P> = {
readonly initialProgress?: P
readonly previous?: Success<A> | Failure<A, E>
readonly previous?: Final<A, E, P>
} & (
| {
readonly refresh: true
readonly previous: Success<A> | Failure<A, E>
readonly previous: Final<A, E, P>
}
| {
readonly refresh?: false

View File

@@ -2,7 +2,7 @@ import { HttpClient, type HttpClientError } from "@effect/platform"
import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
import { Component, ErrorObserver, Query, Result, Subscribable, SubscriptionRef } from "effect-fc"
import { Component, ErrorObserver, Mutation, Query, Result, Subscribable, SubscriptionRef } from "effect-fc"
import { runtime } from "@/runtime"
@@ -16,7 +16,7 @@ const Post = Schema.Struct({
const ResultView = Component.makeUntraced("Result")(function*() {
const runPromise = yield* Component.useRunPromise()
const [idRef, query] = yield* Component.useOnMount(() => Effect.gen(function*() {
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
const idRef = yield* SubscriptionRef.make(1)
const key = Stream.zipLatest(Stream.make("posts" as const), idRef.changes)
@@ -30,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 [result] = yield* Subscribable.useSubscribables([query.result])
const [queryResult, mutationResult] = yield* Subscribable.useSubscribables([query.result, mutation.result])
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
Effect.andThen(observer => observer.subscribe),
@@ -59,23 +68,44 @@ const ResultView = Component.makeUntraced("Result")(function*() {
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
/>
{Match.value(result).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>
{Match.value(queryResult).pipe(
Match.tag("Running", () => <Text>Loading...</Text>),
Match.tag("Success", result => <>
<Heading>{result.value.title}</Heading>
<Text>{result.value.body}</Text>
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
</>),
Match.tag("Failure", result =>
<Text>An error has occured: {result.cause.toString()}</Text>
),
Match.orElse(() => <></>),
)}
</div>
<Flex direction="row" justify="center" align="center" gap="1">
<Button onClick={() => runPromise(query.refresh)}>Refresh</Button>
<Button onClick={() => runPromise(query.refetch)}>Refetch</Button>
</Flex>
<div>
{Match.value(mutationResult).pipe(
Match.tag("Running", () => <Text>Loading...</Text>),
Match.tag("Success", result => <>
<Heading>{result.value.title}</Heading>
<Text>{result.value.body}</Text>
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
</>),
Match.tag("Failure", result =>
<Text>An error has occured: {result.cause.toString()}</Text>
),
Match.orElse(() => <></>),
)}
</div>
<Flex direction="row" justify="center" align="center" gap="1">
<Button onClick={() => runPromise(Effect.andThen(idRef, id => mutation.mutate([id])))}>Mutate</Button>
</Flex>
</Flex>
</Container>
)