From f51b1b04ae184bdbadcdbabf2015fe7def9f0e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 27 Nov 2025 22:21:36 +0100 Subject: [PATCH] Working mutation --- packages/effect-fc/src/Mutation.ts | 17 ++++---- packages/effect-fc/src/Query.ts | 43 ++++++++++++++----- packages/effect-fc/src/Result.ts | 6 ++- packages/example/src/routes/query.tsx | 62 ++++++++++++++++++++------- 4 files changed, 90 insertions(+), 38 deletions(-) diff --git a/packages/effect-fc/src/Mutation.ts b/packages/effect-fc/src/Mutation.ts index 69de1a4..7e026a0 100644 --- a/packages/effect-fc/src/Mutation.ts +++ b/packages/effect-fc/src/Mutation.ts @@ -17,7 +17,7 @@ extends Pipeable.Pipeable { readonly fiber: Subscribable.Subscribable>> readonly result: Subscribable.Subscribable> - mutate(key: K): Effect.Effect> + mutate(key: K): Effect.Effect> mutateSubscribable(key: K): Effect.Effect>> } @@ -37,13 +37,12 @@ extends Pipeable.Class() implements Mutation { super() } - mutate(key: K): Effect.Effect> { + mutate(key: K): Effect.Effect> { 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>> { return Effect.andThen( SubscriptionRef.set(this.latestKey, Option.some(key)), @@ -57,7 +56,7 @@ extends Pipeable.Class() implements Mutation { 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 { watch( sub: Subscribable.Subscribable> - ): Effect.Effect> { + ): Effect.Effect> { return Effect.andThen( sub.get, initial => Stream.runFoldEffect( @@ -92,20 +91,20 @@ extends Pipeable.Class() implements Mutation { initial, (_, result) => Effect.as(SubscriptionRef.set(this.result, result), result), ), - ) + ) as Effect.Effect> } } export const isMutation = (u: unknown): u is Mutation => Predicate.hasProperty(u, MutationTypeId) export declare namespace make { - export interface Options { - readonly f: (key: NoInfer) => Effect.Effect>> + export interface Options { + readonly f: (key: K) => Effect.Effect>> readonly initialProgress?: P } } -export const make = Effect.fnUntraced(function* ( +export const make = Effect.fnUntraced(function* ( options: make.Options ): Effect.fn.Return< Mutation, P>, diff --git a/packages/effect-fc/src/Query.ts b/packages/effect-fc/src/Query.ts index 0ad366e..b04fa49 100644 --- a/packages/effect-fc/src/Query.ts +++ b/packages/effect-fc/src/Query.ts @@ -18,9 +18,12 @@ extends Pipeable.Pipeable { readonly fiber: Subscribable.Subscribable>> readonly result: Subscribable.Subscribable> - fetch(key: K): Effect.Effect> - readonly refetch: Effect.Effect, Cause.NoSuchElementException> - readonly refresh: Effect.Effect, Cause.NoSuchElementException> + fetch(key: K): Effect.Effect> + fetchSubscribable(key: K): Effect.Effect>> + readonly refetch: Effect.Effect, Cause.NoSuchElementException> + readonly refetchSubscribable: Effect.Effect>, Cause.NoSuchElementException> + readonly refresh: Effect.Effect, Cause.NoSuchElementException> + readonly refreshSubscribable: Effect.Effect>, Cause.NoSuchElementException> } export class QueryImpl @@ -47,15 +50,20 @@ extends Pipeable.Class() implements Query { })) } - fetch(key: K): Effect.Effect> { + fetch(key: K): Effect.Effect> { 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, Cause.NoSuchElementException> { + fetchSubscribable(key: K): Effect.Effect>> { + 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, Cause.NoSuchElementException> { return this.interrupt.pipe( Effect.andThen(this.latestKey), Effect.andThen(identity), @@ -63,8 +71,14 @@ extends Pipeable.Class() implements Query { Effect.andThen(sub => this.watch(sub)), ) } - - get refresh(): Effect.Effect, Cause.NoSuchElementException> { + get refetchSubscribable(): Effect.Effect>, 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, Cause.NoSuchElementException> { return this.interrupt.pipe( Effect.andThen(this.latestKey), Effect.andThen(identity), @@ -72,6 +86,13 @@ extends Pipeable.Class() implements Query { Effect.andThen(sub => this.watch(sub)), ) } + get refreshSubscribable(): Effect.Effect>, 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 { 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 { watch( sub: Subscribable.Subscribable> - ): Effect.Effect> { + ): Effect.Effect> { return Effect.andThen( sub.get, initial => Stream.runFoldEffect( @@ -109,7 +130,7 @@ extends Pipeable.Class() implements Query { initial, (_, result) => Effect.as(SubscriptionRef.set(this.result, result), result), ), - ) + ) as Effect.Effect> } } diff --git a/packages/effect-fc/src/Result.ts b/packages/effect-fc/src/Result.ts index 926d55f..cdab4ff 100644 --- a/packages/effect-fc/src/Result.ts +++ b/packages/effect-fc/src/Result.ts @@ -100,6 +100,7 @@ const ResultPrototype = Object.freeze({ export const isResult = (u: unknown): u is Result => Predicate.hasProperty(u, ResultTypeId) +export const isFinal = (u: unknown): u is Final => 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 => isResult(u) && u._tag === "Running" export const isSuccess = (u: unknown): u is Success => isResult(u) && u._tag === "Success" @@ -121,6 +122,7 @@ export const fail = ( cause, previousSuccess: Option.fromNullable(previousSuccess), }, ResultPrototype) + export const refreshing = | Failure, P = never>( result: R, progress?: P, @@ -199,11 +201,11 @@ export namespace unsafeForkEffect { export type Options = { readonly initialProgress?: P - readonly previous?: Success | Failure + readonly previous?: Final } & ( | { readonly refresh: true - readonly previous: Success | Failure + readonly previous: Final } | { readonly refresh?: false diff --git a/packages/example/src/routes/query.tsx b/packages/example/src/routes/query.tsx index eab443b..b8c38f1 100644 --- a/packages/example/src/routes/query.tsx +++ b/packages/example/src/routes/query.tsx @@ -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().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", () => Loading...), - Match.tag("Success", result => <> - {result.value.title} - {result.value.body} - {Result.isRefreshing(result) && Refreshing...} - ), - Match.tag("Failure", result => - An error has occured: {result.cause.toString()} - ), - Match.orElse(() => <>), - )} +
+ {Match.value(queryResult).pipe( + Match.tag("Running", () => Loading...), + Match.tag("Success", result => <> + {result.value.title} + {result.value.body} + {Result.isRefreshing(result) && Refreshing...} + ), + Match.tag("Failure", result => + An error has occured: {result.cause.toString()} + ), + Match.orElse(() => <>), + )} +
+ +
+ {Match.value(mutationResult).pipe( + Match.tag("Running", () => Loading...), + Match.tag("Success", result => <> + {result.value.title} + {result.value.body} + {Result.isRefreshing(result) && Refreshing...} + ), + Match.tag("Failure", result => + An error has occured: {result.cause.toString()} + ), + Match.orElse(() => <>), + )} +
+ + + + )