From d7c648994d3e05e051bfbf8c7d9850e7616b1775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 31 Mar 2025 21:42:24 +0200 Subject: [PATCH] @reffuse/extension-query 0.1.2 (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julien Valverdé Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/8 --- packages/example/src/QueryErrorHandler.tsx | 4 +- packages/example/src/main.tsx | 10 +-- packages/example/src/query.ts | 4 +- packages/example/src/query/reffuse.ts | 4 +- packages/example/src/reffuse.ts | 12 ++-- packages/example/src/todos/reffuse.ts | 4 +- packages/extension-query/package.json | 2 +- packages/extension-query/src/ErrorHandler.ts | 54 --------------- packages/extension-query/src/QueryClient.ts | 64 ++++++++++++------ .../extension-query/src/QueryErrorHandler.ts | 65 +++++++++++++++++++ .../extension-query/src/QueryExtension.ts | 16 ++--- packages/extension-query/src/index.ts | 2 +- .../src/internal/MutationRunner.ts | 40 ++++++------ .../src/internal/QueryRunner.ts | 56 ++++++++-------- 14 files changed, 186 insertions(+), 151 deletions(-) delete mode 100644 packages/extension-query/src/ErrorHandler.ts create mode 100644 packages/extension-query/src/QueryErrorHandler.ts diff --git a/packages/example/src/QueryErrorHandler.tsx b/packages/example/src/QueryErrorHandler.tsx index 2a458ea..5a072ee 100644 --- a/packages/example/src/QueryErrorHandler.tsx +++ b/packages/example/src/QueryErrorHandler.tsx @@ -1,5 +1,5 @@ import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes" -import { ErrorHandler } from "@reffuse/extension-query" +import { QueryErrorHandler } from "@reffuse/extension-query" import { Cause, Console, Context, Effect, Either, flow, Match, Option, Stream } from "effect" import { useState } from "react" import { AppQueryErrorHandler } from "./query" @@ -8,7 +8,7 @@ import { R } from "./reffuse" export function VQueryErrorHandler() { const [failure, setFailure] = useState(Option.none> + QueryErrorHandler.Error> >>()) R.useFork(() => AppQueryErrorHandler.pipe(Effect.flatMap(handler => diff --git a/packages/example/src/main.tsx b/packages/example/src/main.tsx index a730288..a5da407 100644 --- a/packages/example/src/main.tsx +++ b/packages/example/src/main.tsx @@ -6,17 +6,17 @@ import { StrictMode } from "react" import { createRoot } from "react-dom/client" import { ReffuseRuntime } from "reffuse" import { AppQueryClient, AppQueryErrorHandler } from "./query" -import { GlobalContext } from "./reffuse" +import { RootContext } from "./reffuse" import { routeTree } from "./routeTree.gen" const layer = Layer.empty.pipe( + Layer.provideMerge(AppQueryClient.Live), + Layer.provideMerge(AppQueryErrorHandler.Live), Layer.provideMerge(Clipboard.layer), Layer.provideMerge(Geolocation.layer), Layer.provideMerge(Permissions.layer), Layer.provideMerge(FetchHttpClient.layer), - Layer.provideMerge(AppQueryClient.Live), - Layer.provideMerge(AppQueryErrorHandler.Live), ) const router = createRouter({ routeTree }) @@ -31,9 +31,9 @@ declare module "@tanstack/react-router" { createRoot(document.getElementById("root")!).render( - + - + ) diff --git a/packages/example/src/query.ts b/packages/example/src/query.ts index 7b659eb..ecf7e53 100644 --- a/packages/example/src/query.ts +++ b/packages/example/src/query.ts @@ -1,9 +1,9 @@ import { HttpClientError } from "@effect/platform" -import { ErrorHandler, QueryClient } from "@reffuse/extension-query" +import { QueryClient, QueryErrorHandler } from "@reffuse/extension-query" import { Effect } from "effect" -export class AppQueryErrorHandler extends ErrorHandler.Service()( "AppQueryErrorHandler", diff --git a/packages/example/src/query/reffuse.ts b/packages/example/src/query/reffuse.ts index ced8e18..08cf368 100644 --- a/packages/example/src/query/reffuse.ts +++ b/packages/example/src/query/reffuse.ts @@ -1,10 +1,10 @@ -import { GlobalReffuse } from "@/reffuse" +import { RootReffuse } from "@/reffuse" import { Reffuse, ReffuseContext } from "reffuse" import { Uuid4Query } from "./services" export const QueryContext = ReffuseContext.make() -export const R = new class QueryReffuse extends GlobalReffuse.pipe( +export const R = new class QueryReffuse extends RootReffuse.pipe( Reffuse.withContexts(QueryContext) ) {} diff --git a/packages/example/src/reffuse.ts b/packages/example/src/reffuse.ts index f23c07e..25a8fa7 100644 --- a/packages/example/src/reffuse.ts +++ b/packages/example/src/reffuse.ts @@ -6,19 +6,19 @@ import { Reffuse, ReffuseContext } from "reffuse" import { AppQueryClient, AppQueryErrorHandler } from "./query" -export const GlobalContext = ReffuseContext.make< +export const RootContext = ReffuseContext.make< + | AppQueryClient + | AppQueryErrorHandler | Clipboard.Clipboard | Geolocation.Geolocation | Permissions.Permissions | HttpClient.HttpClient - | AppQueryClient - | AppQueryErrorHandler >() -export class GlobalReffuse extends Reffuse.Reffuse.pipe( +export class RootReffuse extends Reffuse.Reffuse.pipe( Reffuse.withExtension(LazyRefExtension), Reffuse.withExtension(QueryExtension), - Reffuse.withContexts(GlobalContext), + Reffuse.withContexts(RootContext), ) {} -export const R = new GlobalReffuse() +export const R = new RootReffuse() diff --git a/packages/example/src/todos/reffuse.ts b/packages/example/src/todos/reffuse.ts index d01357c..986f562 100644 --- a/packages/example/src/todos/reffuse.ts +++ b/packages/example/src/todos/reffuse.ts @@ -1,10 +1,10 @@ -import { GlobalReffuse } from "@/reffuse" +import { RootReffuse } from "@/reffuse" import { Reffuse, ReffuseContext } from "reffuse" import { TodosState } from "./services" export const TodosContext = ReffuseContext.make() -export const R = new class TodosReffuse extends GlobalReffuse.pipe( +export const R = new class TodosReffuse extends RootReffuse.pipe( Reffuse.withContexts(TodosContext) ) {} diff --git a/packages/extension-query/package.json b/packages/extension-query/package.json index 9ac68b8..a28e278 100644 --- a/packages/extension-query/package.json +++ b/packages/extension-query/package.json @@ -1,6 +1,6 @@ { "name": "@reffuse/extension-query", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "files": [ "./README.md", diff --git a/packages/extension-query/src/ErrorHandler.ts b/packages/extension-query/src/ErrorHandler.ts deleted file mode 100644 index 0437901..0000000 --- a/packages/extension-query/src/ErrorHandler.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect" -import type { Mutable } from "effect/Types" - - -export interface ErrorHandler { - readonly errors: Stream.Stream> - readonly handle: (self: Effect.Effect) => Effect.Effect, R> -} - -export type Error = T extends ErrorHandler ? E : never - - -export interface ServiceResult extends Context.TagClass> { - readonly Live: Layer.Layer -} - -export const Service = () => ( - ( - id: Id, - f: ( - self: Effect.Effect, - failure: (failure: E) => Effect.Effect, - defect: (defect: unknown) => Effect.Effect, - ) => Effect.Effect, - ): ServiceResult => { - const TagClass = Context.Tag(id)() as ServiceResult - - (TagClass as Mutable).Live = Layer.effect(TagClass, Effect.gen(function*() { - const queue = yield* Queue.unbounded>() - const errors = Stream.fromQueue(queue) - - const handle = ( - self: Effect.Effect - ): Effect.Effect, R> => f(self as unknown as Effect.Effect, - (failure: E) => Queue.offer(queue, Cause.fail(failure)).pipe( - Effect.andThen(Effect.failCause(Cause.empty)) - ), - (defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe( - Effect.andThen(Effect.failCause(Cause.empty)) - ), - ) - - return { errors, handle } - })) - - return TagClass - } -) - - -export class DefaultErrorHandler extends Service()( - "@reffuse/extension-query/DefaultErrorHandler", - identity, -) {} diff --git a/packages/extension-query/src/QueryClient.ts b/packages/extension-query/src/QueryClient.ts index dd467f5..2b9b003 100644 --- a/packages/extension-query/src/QueryClient.ts +++ b/packages/extension-query/src/QueryClient.ts @@ -1,39 +1,65 @@ -import { Context, Layer } from "effect" +import { Context, Effect, Layer } from "effect" import type { Mutable } from "effect/Types" -import * as ErrorHandler from "./ErrorHandler.js" +import * as QueryErrorHandler from "./QueryErrorHandler.js" -export interface QueryClient { - readonly ErrorHandler: Context.Tag> +export interface QueryClient { + readonly errorHandler: QueryErrorHandler.QueryErrorHandler } const id = "@reffuse/extension-query/QueryClient" -export type TagClassShape = Context.TagClassShape> -export type GenericTagClass = Context.TagClass, typeof id, QueryClient> -export const makeGenericTagClass = (): GenericTagClass => Context.Tag(id)() +export type TagClassShape = Context.TagClassShape> +export type GenericTagClass = Context.TagClass< + TagClassShape, + typeof id, + QueryClient +> +export const makeGenericTagClass = (): GenericTagClass => Context.Tag(id)() -export interface ServiceProps { - readonly ErrorHandler?: Context.Tag> +export interface ServiceProps { + readonly ErrorHandler?: Context.Tag> } -export interface ServiceResult extends Context.TagClass> { - readonly Live: Layer.Layer +export interface ServiceResult extends Context.TagClass< + Self, + typeof id, + QueryClient +> { + readonly Live: Layer.Layer< + Self | (EH extends QueryErrorHandler.DefaultQueryErrorHandler ? EH : never), + never, + EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH + > } export const Service = () => ( < - EH = ErrorHandler.DefaultErrorHandler, - HandledE = ErrorHandler.Error>, + EH = QueryErrorHandler.DefaultQueryErrorHandler, + FallbackA = QueryErrorHandler.Fallback>, + HandledE = QueryErrorHandler.Error>, >( - props?: ServiceProps - ): ServiceResult => { - const TagClass = Context.Tag(id)() as ServiceResult - (TagClass as Mutable).Live = Layer.succeed(TagClass, { - ErrorHandler: (props?.ErrorHandler ?? ErrorHandler.DefaultErrorHandler) as Context.Tag> - }) + props?: ServiceProps + ): ServiceResult => { + const TagClass = Context.Tag(id)() as ServiceResult + + (TagClass as Mutable).Live = Layer.effect(TagClass, Effect.Do.pipe( + Effect.bind("errorHandler", () => + (props?.ErrorHandler ?? QueryErrorHandler.DefaultQueryErrorHandler) as Effect.Effect< + QueryErrorHandler.QueryErrorHandler, + never, + EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH + > + ) + )).pipe( + Layer.provideMerge((props?.ErrorHandler + ? Layer.empty + : QueryErrorHandler.DefaultQueryErrorHandler.Live + ) as Layer.Layer) + ) + return TagClass } ) diff --git a/packages/extension-query/src/QueryErrorHandler.ts b/packages/extension-query/src/QueryErrorHandler.ts new file mode 100644 index 0000000..733b7e1 --- /dev/null +++ b/packages/extension-query/src/QueryErrorHandler.ts @@ -0,0 +1,65 @@ +import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect" +import type { Mutable } from "effect/Types" + + +export interface QueryErrorHandler { + readonly errors: Stream.Stream> + readonly handle: (self: Effect.Effect) => Effect.Effect, R> +} + +export type Fallback = T extends QueryErrorHandler ? A : never +export type Error = T extends QueryErrorHandler ? E : never + + +export interface ServiceResult< + Self, + Id extends string, + FallbackA, + HandledE, +> extends Context.TagClass< + Self, + Id, + QueryErrorHandler +> { + readonly Live: Layer.Layer +} + +export const Service = () => ( + ( + id: Id, + f: ( + self: Effect.Effect, + failure: (failure: HandledE) => Effect.Effect, + defect: (defect: unknown) => Effect.Effect, + ) => Effect.Effect, + ): ServiceResult => { + const TagClass = Context.Tag(id)() as ServiceResult + + (TagClass as Mutable).Live = Layer.effect(TagClass, Effect.gen(function*() { + const queue = yield* Queue.unbounded>() + const errors = Stream.fromQueue(queue) + + const handle = ( + self: Effect.Effect + ): Effect.Effect, R> => f( + self as unknown as Effect.Effect, + (failure: HandledE) => Queue.offer(queue, Cause.fail(failure)).pipe( + Effect.andThen(Effect.failCause(Cause.empty)) + ), + (defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe( + Effect.andThen(Effect.failCause(Cause.empty)) + ), + ) + + return { errors, handle } + })) + + return TagClass + } +) + + +export class DefaultQueryErrorHandler extends Service()( + "@reffuse/extension-query/DefaultQueryErrorHandler", + identity, +) {} diff --git a/packages/extension-query/src/QueryExtension.ts b/packages/extension-query/src/QueryExtension.ts index 5df8c69..eee7860 100644 --- a/packages/extension-query/src/QueryExtension.ts +++ b/packages/extension-query/src/QueryExtension.ts @@ -51,19 +51,19 @@ export interface UseMutationResult { export const QueryExtension = ReffuseExtension.make(() => ({ useQuery< - EH, QK extends readonly unknown[], QA, + FallbackA, QE, HandledE, QR extends R, R, >( - this: ReffuseHelpers.ReffuseHelpers | EH>, + this: ReffuseHelpers.ReffuseHelpers>, props: UseQueryProps, - ): UseQueryResult> { + ): UseQueryResult> { const runner = this.useMemo(() => QueryRunner.make({ - QueryClient: QueryClient.makeGenericTagClass(), + QueryClient: QueryClient.makeGenericTagClass(), key: props.key, query: props.query, }), [props.key]) @@ -90,19 +90,19 @@ export const QueryExtension = ReffuseExtension.make(() => ({ }, useMutation< - EH, QK extends readonly unknown[], QA, + FallbackA, QE, HandledE, QR extends R, R, >( - this: ReffuseHelpers.ReffuseHelpers | EH>, + this: ReffuseHelpers.ReffuseHelpers>, props: UseMutationProps, - ): UseMutationResult> { + ): UseMutationResult> { const runner = this.useMemo(() => MutationRunner.make({ - QueryClient: QueryClient.makeGenericTagClass(), + QueryClient: QueryClient.makeGenericTagClass(), mutation: props.mutation, }), []) diff --git a/packages/extension-query/src/index.ts b/packages/extension-query/src/index.ts index d30d000..4f0feaf 100644 --- a/packages/extension-query/src/index.ts +++ b/packages/extension-query/src/index.ts @@ -1,6 +1,6 @@ -export * as ErrorHandler from "./ErrorHandler.js" export * as MutationService from "./MutationService.js" export * as QueryClient from "./QueryClient.js" +export * as QueryErrorHandler from "./QueryErrorHandler.js" export * from "./QueryExtension.js" export * as QueryProgress from "./QueryProgress.js" export * as QueryService from "./QueryService.js" diff --git a/packages/extension-query/src/internal/MutationRunner.ts b/packages/extension-query/src/internal/MutationRunner.ts index cd29477..3865223 100644 --- a/packages/extension-query/src/internal/MutationRunner.ts +++ b/packages/extension-query/src/internal/MutationRunner.ts @@ -17,33 +17,33 @@ export interface MutationRunner { } -export interface MakeProps { - readonly QueryClient: QueryClient.GenericTagClass +export interface MakeProps { + readonly QueryClient: QueryClient.GenericTagClass readonly mutation: (key: K) => Effect.Effect } -export const make = ( +export const make = ( { QueryClient, mutation, - }: MakeProps + }: MakeProps ): Effect.Effect< - MutationRunner, R>, + MutationRunner, R>, never, - R | QueryClient.TagClassShape | EH + R | QueryClient.TagClassShape > => Effect.gen(function*() { - const context = yield* Effect.context | EH>() - const globalStateRef = yield* SubscriptionRef.make(AsyncData.noData>()) + const context = yield* Effect.context>() + const globalStateRef = yield* SubscriptionRef.make(AsyncData.noData>()) - const queryStateTag = QueryState.makeTag>() + const queryStateTag = QueryState.makeTag>() - const run = (key: K) => Effect.all([ - queryStateTag, - QueryClient.pipe(Effect.flatMap(client => client.ErrorHandler)), - ]).pipe( - Effect.flatMap(([state, errorHandler]) => state.set(AsyncData.loading()).pipe( + const run = (key: K) => Effect.Do.pipe( + Effect.bind("state", () => queryStateTag), + Effect.bind("client", () => QueryClient), + + Effect.flatMap(({ state, client }) => state.set(AsyncData.loading()).pipe( Effect.andThen(mutation(key)), - errorHandler.handle, + client.errorHandler.handle, Effect.matchCauseEffect({ onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe( Effect.tap(state.set) @@ -64,11 +64,11 @@ export const make = ( value => Ref.set(globalStateRef, value), )) - const forkMutate = (...key: K) => Effect.all([ - Ref.make(AsyncData.noData>()), - Queue.unbounded>>(), - ]).pipe( - Effect.flatMap(([stateRef, stateQueue]) => + const forkMutate = (...key: K) => Effect.Do.pipe( + Effect.bind("stateRef", () => Ref.make(AsyncData.noData>())), + Effect.bind("stateQueue", () => Queue.unbounded>>()), + + Effect.flatMap(({ stateRef, stateQueue }) => Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe( Effect.andThen(run(key)), Effect.scoped, diff --git a/packages/extension-query/src/internal/QueryRunner.ts b/packages/extension-query/src/internal/QueryRunner.ts index bc659d3..5b8bab7 100644 --- a/packages/extension-query/src/internal/QueryRunner.ts +++ b/packages/extension-query/src/internal/QueryRunner.ts @@ -31,33 +31,33 @@ export interface QueryRunner { } -export interface MakeProps { - readonly QueryClient: QueryClient.GenericTagClass +export interface MakeProps { + readonly QueryClient: QueryClient.GenericTagClass readonly key: Stream.Stream readonly query: (key: K) => Effect.Effect } -export const make = ( +export const make = ( { QueryClient, key, query, - }: MakeProps + }: MakeProps ): Effect.Effect< - QueryRunner, R>, + QueryRunner, R>, never, - R | QueryClient.TagClassShape | EH + R | QueryClient.TagClassShape > => Effect.gen(function*() { - const context = yield* Effect.context | EH>() + const context = yield* Effect.context>() const latestKeyRef = yield* SubscriptionRef.make(Option.none()) - const stateRef = yield* SubscriptionRef.make(AsyncData.noData>()) + const stateRef = yield* SubscriptionRef.make(AsyncData.noData>()) const fiberRef = yield* SubscriptionRef.make(Option.none | AsyncData.Failure>, + AsyncData.Success | AsyncData.Failure>, Cause.NoSuchElementException >>()) - const queryStateTag = QueryState.makeTag>() + const queryStateTag = QueryState.makeTag>() const interrupt = fiberRef.pipe( Effect.flatMap(Option.match({ @@ -80,30 +80,28 @@ export const make = ( })) ) - const run = Effect.all([ - queryStateTag, - QueryClient.pipe(Effect.flatMap(client => client.ErrorHandler)), - ]).pipe( - Effect.flatMap(([state, errorHandler]) => latestKeyRef.pipe( - Effect.flatMap(identity), - Effect.flatMap(key => query(key).pipe( - errorHandler.handle, - Effect.matchCauseEffect({ - onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe( - Effect.tap(state.set) - ), - onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe( - Effect.tap(state.set) - ), - }), - )), + const run = Effect.Do.pipe( + Effect.bind("state", () => queryStateTag), + Effect.bind("client", () => QueryClient), + Effect.bind("latestKey", () => latestKeyRef.pipe(Effect.flatMap(identity))), + + Effect.flatMap(({ state, client, latestKey }) => query(latestKey).pipe( + client.errorHandler.handle, + Effect.matchCauseEffect({ + onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe( + Effect.tap(state.set) + ), + onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe( + Effect.tap(state.set) + ), + }), )), Effect.provide(context), Effect.provide(QueryProgress.QueryProgress.Live), ) - const forkFetch = Queue.unbounded>>().pipe( + const forkFetch = Queue.unbounded>>().pipe( Effect.flatMap(stateQueue => queryStateTag.pipe( Effect.flatMap(state => interrupt.pipe( Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe( @@ -139,7 +137,7 @@ export const make = ( })) ) - const forkRefresh = Queue.unbounded>>().pipe( + const forkRefresh = Queue.unbounded>>().pipe( Effect.flatMap(stateQueue => interrupt.pipe( Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe( Effect.andThen(Queue.shutdown(stateQueue))