From baa8c922210c1112ced7d0c1e5cf38b3ad8319b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 24 Mar 2025 12:03:55 +0100 Subject: [PATCH] Query refactoring --- .../src/query/views/Uuid4QueryService.tsx | 4 +- .../example/src/routes/query/usequery.tsx | 4 +- .../extension-query/src/QueryExtension.ts | 13 +- packages/extension-query/src/QueryService.ts | 7 +- .../src/internal/QueryRunner.ts | 111 +++++++++++------- 5 files changed, 87 insertions(+), 52 deletions(-) diff --git a/packages/example/src/query/views/Uuid4QueryService.tsx b/packages/example/src/query/views/Uuid4QueryService.tsx index bc0eada..ff92397 100644 --- a/packages/example/src/query/views/Uuid4QueryService.tsx +++ b/packages/example/src/query/views/Uuid4QueryService.tsx @@ -5,7 +5,7 @@ import { Uuid4Query } from "../services" export function Uuid4QueryService() { - const runSync = R.useRunSync() + const runFork = R.useRunFork() const query = R.useMemo(() => Uuid4Query.Uuid4Query, []) const [state] = R.useRefState(query.state) @@ -25,7 +25,7 @@ export function Uuid4QueryService() { })} - + ) diff --git a/packages/example/src/routes/query/usequery.tsx b/packages/example/src/routes/query/usequery.tsx index 1f76770..8c50f29 100644 --- a/packages/example/src/routes/query/usequery.tsx +++ b/packages/example/src/routes/query/usequery.tsx @@ -15,7 +15,7 @@ export const Route = createFileRoute("/query/usequery")({ const Result = Schema.Array(Schema.String) function RouteComponent() { - const runSync = R.useRunSync() + const runFork = R.useRunFork() const [count, setCount] = useState(1) @@ -59,7 +59,7 @@ function RouteComponent() { })} - + ) diff --git a/packages/extension-query/src/QueryExtension.ts b/packages/extension-query/src/QueryExtension.ts index 44d462f..5df8c69 100644 --- a/packages/extension-query/src/QueryExtension.ts +++ b/packages/extension-query/src/QueryExtension.ts @@ -18,7 +18,11 @@ export interface UseQueryProps { export interface UseQueryResult { readonly latestKey: SubscriptionRef.SubscriptionRef> readonly state: SubscriptionRef.SubscriptionRef> - readonly refresh: Effect.Effect> + + readonly forkRefresh: Effect.Effect | AsyncData.Failure, Cause.NoSuchElementException>, + state: Stream.Stream>, + ]> readonly layer: ( tag: Context.TagClass> @@ -32,6 +36,7 @@ export interface UseMutationProps { export interface UseMutationResult { readonly state: SubscriptionRef.SubscriptionRef> + readonly mutate: (...key: K) => Effect.Effect | AsyncData.Failure> readonly forkMutate: (...key: K) => Effect.Effect | AsyncData.Failure>, @@ -73,12 +78,13 @@ export const QueryExtension = ReffuseExtension.make(() => ({ return React.useMemo(() => ({ latestKey: runner.latestKeyRef, state: runner.stateRef, - refresh: runner.forkRefresh, + + forkRefresh: runner.forkRefresh, layer: tag => Layer.succeed(tag, { latestKey: runner.latestKeyRef, state: runner.stateRef, - refresh: runner.forkRefresh, + forkRefresh: runner.forkRefresh, }), }), [runner]) }, @@ -102,6 +108,7 @@ export const QueryExtension = ReffuseExtension.make(() => ({ return React.useMemo(() => ({ state: runner.stateRef, + mutate: runner.mutate, forkMutate: runner.forkMutate, diff --git a/packages/extension-query/src/QueryService.ts b/packages/extension-query/src/QueryService.ts index 38235de..ef447b9 100644 --- a/packages/extension-query/src/QueryService.ts +++ b/packages/extension-query/src/QueryService.ts @@ -1,11 +1,14 @@ import type * as AsyncData from "@typed/async-data" -import { type Cause, Effect, type Fiber, type Option, type SubscriptionRef } from "effect" +import { type Cause, Effect, type Fiber, type Option, type Stream, type SubscriptionRef } from "effect" export interface QueryService { readonly latestKey: SubscriptionRef.SubscriptionRef> readonly state: SubscriptionRef.SubscriptionRef> - readonly refresh: Effect.Effect> + readonly forkRefresh: Effect.Effect | AsyncData.Failure, Cause.NoSuchElementException>, + state: Stream.Stream>, + ]> } export const Tag = (id: Id) => < diff --git a/packages/extension-query/src/internal/QueryRunner.ts b/packages/extension-query/src/internal/QueryRunner.ts index c96ec9d..15148bc 100644 --- a/packages/extension-query/src/internal/QueryRunner.ts +++ b/packages/extension-query/src/internal/QueryRunner.ts @@ -1,6 +1,6 @@ import { BrowserStream } from "@effect/platform-browser" import * as AsyncData from "@typed/async-data" -import { type Cause, type Context, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect" +import { type Cause, type Context, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect" import type * as QueryClient from "../QueryClient.js" import * as QueryProgress from "../QueryProgress.js" import * as QueryState from "./QueryState.js" @@ -11,11 +11,20 @@ export interface QueryRunner { readonly latestKeyRef: SubscriptionRef.SubscriptionRef> readonly stateRef: SubscriptionRef.SubscriptionRef> - readonly fiberRef: SubscriptionRef.SubscriptionRef>> + readonly fiberRef: SubscriptionRef.SubscriptionRef | AsyncData.Failure, + Cause.NoSuchElementException + >>> readonly forkInterrupt: Effect.Effect> - readonly forkFetch: Effect.Effect> - readonly forkRefresh: Effect.Effect> + readonly forkFetch: Effect.Effect | AsyncData.Failure, Cause.NoSuchElementException>, + state: Stream.Stream>, + ]> + readonly forkRefresh: Effect.Effect | AsyncData.Failure, Cause.NoSuchElementException>, + state: Stream.Stream>, + ]> readonly fetchOnKeyChange: Effect.Effect readonly refreshOnWindowFocus: Effect.Effect @@ -43,10 +52,12 @@ export const make = ( const latestKeyRef = yield* SubscriptionRef.make(Option.none()) const stateRef = yield* SubscriptionRef.make(AsyncData.noData>()) - const fiberRef = yield* SubscriptionRef.make(Option.none>()) + const fiberRef = yield* SubscriptionRef.make(Option.none | AsyncData.Failure>, + Cause.NoSuchElementException + >>()) const queryStateTag = QueryState.makeTag>() - const queryStateLayer = QueryState.layer(queryStateTag, stateRef, value => Ref.set(stateRef, value)) const interrupt = fiberRef.pipe( Effect.flatMap(Option.match({ @@ -78,8 +89,12 @@ export const make = ( Effect.flatMap(key => query(key).pipe( errorHandler.handle, Effect.matchCauseEffect({ - onSuccess: v => state.set(AsyncData.success(v)), - onFailure: c => state.set(AsyncData.failure(c)), + onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe( + Effect.tap(state.set) + ), + onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe( + Effect.tap(state.set) + ), }), )), )), @@ -88,48 +103,58 @@ export const make = ( Effect.provide(QueryProgress.QueryProgress.Live), ) - const forkFetch = queryStateTag.pipe( - Effect.flatMap(state => interrupt.pipe( - Effect.andThen(state.set(AsyncData.loading()).pipe( - Effect.andThen(run), - Effect.fork, + const forkFetch = Queue.unbounded>>().pipe( + Effect.flatMap(stateQueue => queryStateTag.pipe( + Effect.flatMap(state => interrupt.pipe( + Effect.andThen(state.set(AsyncData.loading()).pipe( + Effect.andThen(run), + Effect.tap(() => Ref.set(fiberRef, Option.none())), + Effect.forkDaemon, + )), + + Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))), + Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const), )), - )), - Effect.flatMap(fiber => - Ref.set(fiberRef, Option.some(fiber)).pipe( - Effect.andThen(Fiber.join(fiber)), - Effect.andThen(Ref.set(fiberRef, Option.none())), - ) - ), - - Effect.forkDaemon, - Effect.provide(queryStateLayer), + Effect.provide(QueryState.layer( + queryStateTag, + stateRef, + value => Queue.offer(stateQueue, value).pipe( + Effect.andThen(Ref.set(stateRef, value)) + ), + )), + )) ) - const forkRefresh = queryStateTag.pipe( - Effect.flatMap(state => interrupt.pipe( - Effect.andThen(state.update(previous => { - if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous)) - return AsyncData.refreshing(previous) - if (AsyncData.isRefreshing(previous)) - return AsyncData.refreshing(previous.previous) - return AsyncData.loading() - }).pipe( + const setInitialRefreshState = queryStateTag.pipe( + Effect.flatMap(state => state.update(previous => { + if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous)) + return AsyncData.refreshing(previous) + if (AsyncData.isRefreshing(previous)) + return AsyncData.refreshing(previous.previous) + return AsyncData.loading() + })), + ) + + const forkRefresh = Queue.unbounded>>().pipe( + Effect.flatMap(stateQueue => interrupt.pipe( + Effect.andThen(setInitialRefreshState.pipe( Effect.andThen(run), - Effect.fork, - )) - )), + Effect.tap(() => Ref.set(fiberRef, Option.none())), + Effect.forkDaemon, + )), - Effect.flatMap(fiber => - Ref.set(fiberRef, Option.some(fiber)).pipe( - Effect.andThen(Fiber.join(fiber)), - Effect.andThen(Ref.set(fiberRef, Option.none())), - ) - ), + Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))), + Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const), - Effect.forkDaemon, - Effect.provide(queryStateLayer), + Effect.provide(QueryState.layer( + queryStateTag, + stateRef, + value => Queue.offer(stateQueue, value).pipe( + Effect.andThen(Ref.set(stateRef, value)) + ), + )), + )) ) const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(