From 3fa9b7d82153d041aeb12f93ecc629a573c13d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Sun, 2 Mar 2025 20:14:45 +0100 Subject: [PATCH] Working query --- packages/example/src/routes/query.tsx | 5 +- packages/extension-query/src/Query.ts | 35 ---------- packages/extension-query/src/QueryRunner.ts | 75 +++++++++++++++++++++ packages/extension-query/src/index.ts | 64 +++++------------- 4 files changed, 94 insertions(+), 85 deletions(-) delete mode 100644 packages/extension-query/src/Query.ts create mode 100644 packages/extension-query/src/QueryRunner.ts diff --git a/packages/example/src/routes/query.tsx b/packages/example/src/routes/query.tsx index c8f1374..efa1d87 100644 --- a/packages/example/src/routes/query.tsx +++ b/packages/example/src/routes/query.tsx @@ -16,12 +16,13 @@ const Result = Schema.Tuple(Schema.String) function RouteComponent() { const runSync = R.useRunSync() - const { state, triggerRefresh } = R.useQuery({ + const { state, refresh } = R.useQuery({ effect: () => HttpClient.get("https://www.uuidtools.com/api/generate/v4").pipe( HttpClient.withTracerPropagation(false), Effect.flatMap(res => res.json), Effect.flatMap(Schema.decodeUnknown(Result)), Effect.delay("500 millis"), + Effect.scoped, ), deps: [], }) @@ -43,7 +44,7 @@ function RouteComponent() { })} - + ) diff --git a/packages/extension-query/src/Query.ts b/packages/extension-query/src/Query.ts deleted file mode 100644 index 0cf4bdf..0000000 --- a/packages/extension-query/src/Query.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Effect, Fiber, Option, SubscriptionRef, type Ref } from "effect" -import * as AsyncData from "@typed/async-data" - - -export interface QueryRunner { - readonly stateRef: SubscriptionRef.SubscriptionRef> - readonly fiberRef: SubscriptionRef.SubscriptionRef>> - - readonly interrupt: Effect.Effect - fetch(effect: Effect.Effect): Effect.Effect - // refetch(effect: Effect.Effect): Effect.Effect -} - -export const make = Effect.fnUntraced(function*(): Effect.Effect> { - const stateRef = yield* SubscriptionRef.make(AsyncData.noData()) - const fiberRef = yield* SubscriptionRef.make(Option.none>()) - - const interrupt = fiberRef.pipe( - Effect.flatMap(Option.match({ - onSome: Fiber.interrupt, - onNone: () => Effect.void, - })) - ) - - const fetch = Effect.fnUntraced(function*(effect: Effect.Effect) { - - }) - - return { - stateRef, - fiberRef, - interrupt, - fetch, - } -}) diff --git a/packages/extension-query/src/QueryRunner.ts b/packages/extension-query/src/QueryRunner.ts new file mode 100644 index 0000000..ddf931d --- /dev/null +++ b/packages/extension-query/src/QueryRunner.ts @@ -0,0 +1,75 @@ +import * as AsyncData from "@typed/async-data" +import { Effect, Fiber, flow, identity, Option, Ref, SubscriptionRef } from "effect" + + +export interface QueryRunner { + readonly queryRef: SubscriptionRef.SubscriptionRef> + readonly stateRef: SubscriptionRef.SubscriptionRef> + readonly fiberRef: SubscriptionRef.SubscriptionRef>> + + readonly interrupt: Effect.Effect + readonly forkFetch: Effect.Effect + readonly forkRefetch: Effect.Effect +} + + +export const make = ( + query: Effect.Effect +): Effect.Effect, never, R> => Effect.gen(function*() { + const context = yield* Effect.context() + + const queryRef = yield* SubscriptionRef.make(query) + const stateRef = yield* SubscriptionRef.make(AsyncData.noData()) + const fiberRef = yield* SubscriptionRef.make(Option.none>()) + + const interrupt = fiberRef.pipe( + Effect.flatMap(Option.match({ + onSome: flow( + Fiber.interrupt, + Effect.andThen(Ref.set(fiberRef, Option.none())), + ), + onNone: () => Effect.void, + })) + ) + + const forkFetch = interrupt.pipe( + Effect.andThen(Ref.set(stateRef, AsyncData.loading())), + Effect.andThen(queryRef.pipe(Effect.flatMap(identity))), + Effect.matchCauseEffect({ + onSuccess: v => Ref.set(stateRef, AsyncData.success(v)), + onFailure: c => Ref.set(stateRef, AsyncData.failure(c)), + }), + Effect.provide(context), + Effect.forkDaemon, + + Effect.flatMap(fiber => Ref.set(fiberRef, Option.some(fiber))), + ) + + const forkRefetch = interrupt.pipe( + Effect.andThen(Ref.update(stateRef, previous => { + if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous)) + return AsyncData.refreshing(previous) + if (AsyncData.isRefreshing(previous)) + return AsyncData.refreshing(previous.previous) + return AsyncData.loading() + })), + Effect.andThen(queryRef.pipe(Effect.flatMap(identity))), + Effect.matchCauseEffect({ + onSuccess: v => Ref.set(stateRef, AsyncData.success(v)), + onFailure: c => Ref.set(stateRef, AsyncData.failure(c)), + }), + Effect.provide(context), + Effect.forkDaemon, + + Effect.flatMap(fiber => Ref.set(fiberRef, Option.some(fiber))), + ) + + return { + queryRef, + stateRef, + fiberRef, + interrupt, + forkFetch, + forkRefetch, + } +}) diff --git a/packages/extension-query/src/index.ts b/packages/extension-query/src/index.ts index c2731f8..05797e8 100644 --- a/packages/extension-query/src/index.ts +++ b/packages/extension-query/src/index.ts @@ -1,71 +1,39 @@ import * as AsyncData from "@typed/async-data" -import { Effect, Fiber, Option, Ref, Scope, SubscriptionRef } from "effect" +import { Effect, Ref, SubscriptionRef } from "effect" import * as React from "react" import { ReffuseExtension, type ReffuseHelpers } from "reffuse" +import * as QueryRunner from "./QueryRunner.js" export interface UseQueryProps { - effect: () => Effect.Effect + effect: () => Effect.Effect readonly deps: React.DependencyList } export interface UseQueryResult { readonly state: SubscriptionRef.SubscriptionRef> - readonly triggerRefresh: Effect.Effect + readonly refresh: Effect.Effect } -const interruptRunningQuery = (fiberRef: Ref.Ref>>) => fiberRef.pipe( - Effect.flatMap(Option.match({ - onSome: Fiber.interrupt, - onNone: () => Effect.void, - })) -) - -const runQuery = ( - effect: Effect.Effect, - stateRef: Ref.Ref>, -) => effect.pipe( - Effect.matchCauseEffect({ - onSuccess: v => Ref.set(stateRef, AsyncData.success(v)), - onFailure: c => Ref.set(stateRef, AsyncData.failure(c)), - }) -) - - export const QueryExtension = ReffuseExtension.make(() => ({ useQuery( this: ReffuseHelpers.ReffuseHelpers, props: UseQueryProps, ): UseQueryResult { - const context = this.useContext() + const runner = this.useMemo(() => QueryRunner.make(props.effect()), []) - const fiberRef = this.useRef(Option.none>()) - const stateRef = this.useRef(AsyncData.noData()) + this.useFork(() => Effect.addFinalizer(() => runner.interrupt).pipe( + Effect.andThen(Ref.set(runner.queryRef, props.effect())), + Effect.andThen(runner.forkFetch), + ), [runner, ...props.deps]) - const triggerRefresh = React.useMemo(() => interruptRunningQuery(fiberRef).pipe( - Effect.andThen(Ref.update(stateRef, prev => - AsyncData.isSuccess(prev) || AsyncData.isFailure(prev) - ? AsyncData.refreshing(prev) - : AsyncData.loading() - )), - Effect.andThen(runQuery(props.effect(), stateRef)), - Effect.provide(context), - Effect.scoped, - Effect.forkDaemon, - - Effect.flatMap(fiber => Ref.set(fiberRef, Option.some(fiber))), - ), [stateRef, context, fiberRef]) - - this.useEffect(() => interruptRunningQuery(fiberRef).pipe( - Effect.andThen(Ref.set(stateRef, AsyncData.loading())), - Effect.andThen(runQuery(props.effect(), stateRef)), - Effect.scoped, - Effect.forkDaemon, - - Effect.flatMap(fiber => Ref.set(fiberRef, Option.some(fiber))), - ), [...props.deps, stateRef, fiberRef]) - - return React.useMemo(() => ({ state: stateRef, triggerRefresh }), [stateRef, triggerRefresh]) + return React.useMemo(() => ({ + state: runner.stateRef, + refresh: runner.forkRefetch, + }), [runner]) } })) + + +export * as QueryRunner from "./QueryRunner.js"