24 Commits

Author SHA1 Message Date
Julien Valverdé
e9e17ac211 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-16 04:11:25 +01:00
Julien Valverdé
1f0ff725ff Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-16 04:05:39 +01:00
Julien Valverdé
447d89982c Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-16 03:34:54 +01:00
Julien Valverdé
778ee27795 ErrorHandler refactoring
All checks were successful
Lint / lint (push) Successful in 16s
2025-03-16 03:33:01 +01:00
Julien Valverdé
077816efb6 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-16 03:23:12 +01:00
Julien Valverdé
e4bacd1ca7 Working QueryClient refactoring
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-16 03:19:12 +01:00
Julien Valverdé
0e2c0db28f QueryClient refactoring
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-16 02:52:49 +01:00
Julien Valverdé
c943d81702 QueryClient.make
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-15 22:27:15 +01:00
Julien Valverdé
c2bc406a5f Fixed query error handler
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-15 06:43:47 +01:00
Julien Valverdé
4e778b6c95 VQueryErrorHandler
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-15 05:12:38 +01:00
Julien Valverdé
0437fa5dcc QueryErrorHandler work
All checks were successful
Lint / lint (push) Successful in 16s
2025-03-15 02:30:37 +01:00
Julien Valverdé
5614b8df38 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-15 00:52:00 +01:00
Julien Valverdé
70b6c4434e Tests
All checks were successful
Lint / lint (push) Successful in 16s
2025-03-14 22:07:53 +01:00
Julien Valverdé
2e8dfbc988 QueryClient
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-14 22:00:53 +01:00
Julien Valverdé
abc47c4647 Fix
Some checks failed
Lint / lint (push) Failing after 12s
2025-03-14 05:04:49 +01:00
Julien Valverdé
eedd2a7f2a makeTag
Some checks failed
Lint / lint (push) Failing after 12s
2025-03-14 04:57:07 +01:00
Julien Valverdé
f4ab575a8d QueryExtension work
Some checks failed
Lint / lint (push) Failing after 13s
2025-03-14 04:24:56 +01:00
Julien Valverdé
747e2c6056 Done QueryClient
All checks were successful
Lint / lint (push) Successful in 14s
2025-03-14 04:13:14 +01:00
Julien Valverdé
68c68417d8 QueryClient work
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-14 03:56:54 +01:00
Julien Valverdé
ed384a62a8 QueryClient work
Some checks failed
Lint / lint (push) Failing after 15s
2025-03-14 03:26:28 +01:00
Julien Valverdé
3a1748bb39 QueryClient tests
All checks were successful
Lint / lint (push) Successful in 18s
2025-03-13 22:31:50 +01:00
Julien Valverdé
66b8fd2c2e Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-12 06:37:39 +01:00
Julien Valverdé
bc81c443ab Query work
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-11 21:19:57 +01:00
Julien Valverdé
ee5dbe3766 Query work
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-11 20:39:56 +01:00
11 changed files with 210 additions and 51 deletions

View File

@@ -0,0 +1,50 @@
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
import { ErrorHandler } from "@reffuse/extension-query"
import { Cause, Chunk, Context, Effect, Match, Option, Stream } from "effect"
import { useState } from "react"
import { AppQueryErrorHandler } from "./query"
import { R } from "./reffuse"
export function VQueryErrorHandler() {
const [failure, setFailure] = useState(Option.none<Cause.Cause<
ErrorHandler.Error<Context.Tag.Service<AppQueryErrorHandler>>
>>())
R.useFork(() => AppQueryErrorHandler.pipe(Effect.flatMap(handler =>
Stream.runForEach(handler.errors, v => Effect.sync(() =>
setFailure(Option.some(v))
))
)), [])
return Option.match(failure, {
onSome: v => (
<AlertDialog.Root open>
<AlertDialog.Content maxWidth="450px">
<AlertDialog.Title>Error</AlertDialog.Title>
<AlertDialog.Description size="2">
{Cause.failures(v).pipe(
Chunk.head,
Option.getOrThrow,
Match.value,
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
Match.exhaustive,
)}
</AlertDialog.Description>
<Flex gap="3" mt="4" justify="end">
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={() => setFailure(Option.none())}>
Ok
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
),
onNone: () => <></>,
})
}

View File

@@ -5,6 +5,7 @@ import { Layer } from "effect"
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { ReffuseRuntime } from "reffuse" import { ReffuseRuntime } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query"
import { GlobalContext } from "./reffuse" import { GlobalContext } from "./reffuse"
import { routeTree } from "./routeTree.gen" import { routeTree } from "./routeTree.gen"
@@ -14,6 +15,8 @@ const layer = Layer.empty.pipe(
Layer.provideMerge(Geolocation.layer), Layer.provideMerge(Geolocation.layer),
Layer.provideMerge(Permissions.layer), Layer.provideMerge(Permissions.layer),
Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(FetchHttpClient.layer),
Layer.provideMerge(AppQueryClient.Live),
Layer.provideMerge(AppQueryErrorHandler.Live),
) )
const router = createRouter({ routeTree }) const router = createRouter({ routeTree })

View File

@@ -0,0 +1,9 @@
import { HttpClientError } from "@effect/platform"
import { ErrorHandler, QueryClient } from "@reffuse/extension-query"
export class AppQueryErrorHandler extends ErrorHandler.Service("AppQueryErrorHandler")<AppQueryErrorHandler,
HttpClientError.HttpClientError
>() {}
export class AppQueryClient extends QueryClient.Service({ ErrorHandler: AppQueryErrorHandler })<AppQueryClient>() {}

View File

@@ -1,4 +1,3 @@
import { HttpClientError } from "@effect/platform"
import { QueryService } from "@reffuse/extension-query" import { QueryService } from "@reffuse/extension-query"
import { ParseResult, Schema } from "effect" import { ParseResult, Schema } from "effect"
@@ -8,5 +7,5 @@ export const Result = Schema.Array(Schema.String)
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query, export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
readonly ["uuid4", number], readonly ["uuid4", number],
typeof Result.Type, typeof Result.Type,
HttpClientError.HttpClientError | ParseResult.ParseError ParseResult.ParseError
>() {} >() {}

View File

@@ -3,6 +3,7 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { LazyRefExtension } from "@reffuse/extension-lazyref" import { LazyRefExtension } from "@reffuse/extension-lazyref"
import { QueryExtension } from "@reffuse/extension-query" import { QueryExtension } from "@reffuse/extension-query"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query"
export const GlobalContext = ReffuseContext.make< export const GlobalContext = ReffuseContext.make<
@@ -10,6 +11,8 @@ export const GlobalContext = ReffuseContext.make<
| Geolocation.Geolocation | Geolocation.Geolocation
| Permissions.Permissions | Permissions.Permissions
| HttpClient.HttpClient | HttpClient.HttpClient
| AppQueryClient
| AppQueryErrorHandler
>() >()
export class GlobalReffuse extends Reffuse.Reffuse.pipe( export class GlobalReffuse extends Reffuse.Reffuse.pipe(

View File

@@ -1,3 +1,4 @@
import { VQueryErrorHandler } from "@/QueryErrorHandler"
import { Container, Flex, Theme } from "@radix-ui/themes" import { Container, Flex, Theme } from "@radix-ui/themes"
import { createRootRoute, Link, Outlet } from "@tanstack/react-router" import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools" import { TanStackRouterDevtools } from "@tanstack/router-devtools"
@@ -26,6 +27,8 @@ function Root() {
</Container> </Container>
<Outlet /> <Outlet />
<VQueryErrorHandler />
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</Theme> </Theme>
) )

View File

@@ -0,0 +1,37 @@
import { type Cause, Context, Effect, Layer, Queue, Stream } from "effect"
import type { Mutable } from "effect/Types"
export interface ErrorHandler<E> {
readonly errors: Stream.Stream<Cause.Cause<E>>
readonly handle: <A, SelfE, R>(self: Effect.Effect<A, SelfE, R>) => Effect.Effect<A, Exclude<SelfE, E>, R>
}
export type Error<T> = T extends ErrorHandler<infer E> ? E : never
export interface ServiceResult<Self, Id extends string, E> extends Context.TagClass<Self, Id, ErrorHandler<E>> {
readonly Live: Layer.Layer<Self>
}
export const Service = <const Id extends string>(id: Id) => (
<Self, E = never>(): ServiceResult<Self, Id, E> => {
const TagClass = Context.Tag(id)() as ServiceResult<Self, Id, E>
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.gen(function*() {
const queue = yield* Queue.unbounded<Cause.Cause<E>>()
const errors = Stream.fromQueue(queue)
const handle = <A, SelfE, R>(
self: Effect.Effect<A, SelfE, R>
) => Effect.tapErrorCause(self, cause =>
Queue.offer(queue, cause as Cause.Cause<E>)
) as Effect.Effect<A, Exclude<SelfE, E>, R>
return { errors, handle }
}))
return TagClass
}
)
export class DefaultErrorHandler extends Service("@reffuse/extension-query/DefaultErrorHandler")<DefaultErrorHandler>() {}

View File

@@ -0,0 +1,39 @@
import { Context, Layer } from "effect"
import type { Mutable } from "effect/Types"
import * as ErrorHandler from "./ErrorHandler.js"
export interface QueryClient<EH, HandledE> {
readonly ErrorHandler: Context.Tag<EH, ErrorHandler.ErrorHandler<HandledE>>
}
const id = "@reffuse/extension-query/QueryClient"
export type TagClassShape<EH, HandledE> = Context.TagClassShape<typeof id, QueryClient<EH, HandledE>>
export type GenericTagClass<EH, HandledE> = Context.TagClass<TagClassShape<EH, HandledE>, typeof id, QueryClient<EH, HandledE>>
export const makeGenericTagClass = <EH = never, HandledE = never>(): GenericTagClass<EH, HandledE> => Context.Tag(id)()
export interface ServiceProps<EH, HandledE> {
readonly ErrorHandler?: Context.Tag<EH, ErrorHandler.ErrorHandler<HandledE>>
}
export interface ServiceResult<Self, EH, HandledE> extends Context.TagClass<Self, typeof id, QueryClient<EH, HandledE>> {
readonly Live: Layer.Layer<Self>
}
export const Service = <
EH = ErrorHandler.DefaultErrorHandler,
HandledE = ErrorHandler.Error<Context.Tag.Service<ErrorHandler.DefaultErrorHandler>>,
>(
props?: ServiceProps<EH, HandledE>
) => (
<Self>(): ServiceResult<Self, EH, HandledE> => {
const TagClass = Context.Tag(id)() as ServiceResult<Self, EH, HandledE>
(TagClass as Mutable<typeof TagClass>).Live = Layer.succeed(TagClass, {
ErrorHandler: (props?.ErrorHandler ?? ErrorHandler.DefaultErrorHandler) as Context.Tag<EH, ErrorHandler.ErrorHandler<HandledE>>
})
return TagClass
}
)

View File

@@ -2,6 +2,7 @@ import type * as AsyncData from "@typed/async-data"
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect" import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import { ReffuseExtension, type ReffuseHelpers } from "reffuse" import { ReffuseExtension, type ReffuseHelpers } from "reffuse"
import * as QueryClient from "./QueryClient.js"
import * as QueryRunner from "./QueryRunner.js" import * as QueryRunner from "./QueryRunner.js"
import type * as QueryService from "./QueryService.js" import type * as QueryService from "./QueryService.js"
@@ -24,11 +25,20 @@ export interface UseQueryResult<K extends readonly unknown[], A, E> {
export const QueryExtension = ReffuseExtension.make(() => ({ export const QueryExtension = ReffuseExtension.make(() => ({
useQuery<K extends readonly unknown[], A, E, R>( useQuery<
this: ReffuseHelpers.ReffuseHelpers<R>, EH,
props: UseQueryProps<K, A, E, R>, QK extends readonly unknown[],
): UseQueryResult<K, A, E> { QA,
QE,
HandledE,
QR extends R,
R,
>(
this: ReffuseHelpers.ReffuseHelpers<R | QueryClient.TagClassShape<EH, HandledE> | EH>,
props: UseQueryProps<QK, QA, QE, QR>,
): UseQueryResult<QK, QA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => QueryRunner.make({ const runner = this.useMemo(() => QueryRunner.make({
QueryClient: QueryClient.makeGenericTagClass<EH, HandledE>(),
key: props.key, key: props.key,
query: props.query, query: props.query,
}), [props.key]) }), [props.key])

View File

@@ -1,10 +1,11 @@
import { BrowserStream } from "@effect/platform-browser" import { BrowserStream } from "@effect/platform-browser"
import * as AsyncData from "@typed/async-data" import * as AsyncData from "@typed/async-data"
import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect" import { type Cause, type Context, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
import type * as QueryClient from "./QueryClient.js"
export interface QueryRunner<K extends readonly unknown[], A, E, R> { export interface QueryRunner<K extends readonly unknown[], A, E, R> {
readonly query: (key: K) => Effect.Effect<A, E, R> readonly context: Context.Context<R>
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>> readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>> readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
@@ -19,18 +20,27 @@ export interface QueryRunner<K extends readonly unknown[], A, E, R> {
} }
export interface MakeProps<K extends readonly unknown[], A, E, R> { export interface MakeProps<EH, K extends readonly unknown[], A, E, HandledE, R> {
readonly QueryClient: QueryClient.GenericTagClass<EH, HandledE>
readonly key: Stream.Stream<K> readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R> readonly query: (key: K) => Effect.Effect<A, E, R>
} }
export const make = <K extends readonly unknown[], A, E, R>( export const make = <EH, K extends readonly unknown[], A, E, HandledE, R>(
{ key, query }: MakeProps<K, A, E, R> {
): Effect.Effect<QueryRunner<K, A, E, R>, never, R> => Effect.gen(function*() { QueryClient,
const context = yield* Effect.context<R>() key,
query,
}: MakeProps<EH, K, A, E, HandledE, R>
): Effect.Effect<
QueryRunner<K, A, Exclude<E, HandledE>, R>,
never,
R | QueryClient.TagClassShape<EH, HandledE> | EH
> => Effect.gen(function*() {
const context = yield* Effect.context<R | QueryClient.TagClassShape<EH, HandledE> | EH>()
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>()) const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>()) const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, Exclude<E, HandledE>>())
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>()) const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>())
const interrupt = fiberRef.pipe( const interrupt = fiberRef.pipe(
@@ -54,22 +64,27 @@ export const make = <K extends readonly unknown[], A, E, R>(
})) }))
) )
const forkFetch = interrupt.pipe( const run = QueryClient.pipe(
Effect.andThen( Effect.flatMap(client => client.ErrorHandler),
Ref.set(stateRef, AsyncData.loading()).pipe( Effect.flatMap(errorHandler => latestKeyRef.pipe(
Effect.andThen(latestKeyRef),
Effect.flatMap(identity), Effect.flatMap(identity),
Effect.flatMap(key => query(key).pipe( Effect.flatMap(key => query(key).pipe(
errorHandler.handle,
Effect.matchCauseEffect({ Effect.matchCauseEffect({
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)), onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)), onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
}) }),
)),
)), )),
Effect.provide(context), Effect.provide(context),
Effect.fork,
) )
),
const forkFetch = interrupt.pipe(
Effect.andThen(Ref.set(stateRef, AsyncData.loading()).pipe(
Effect.andThen(run),
Effect.fork,
)),
Effect.flatMap(fiber => Effect.flatMap(fiber =>
Ref.set(fiberRef, Option.some(fiber)).pipe( Ref.set(fiberRef, Option.some(fiber)).pipe(
@@ -82,27 +97,16 @@ export const make = <K extends readonly unknown[], A, E, R>(
) )
const forkRefresh = interrupt.pipe( const forkRefresh = interrupt.pipe(
Effect.andThen( Effect.andThen(Ref.update(stateRef, previous => {
Ref.update(stateRef, previous => {
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous)) if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
return AsyncData.refreshing(previous) return AsyncData.refreshing(previous)
if (AsyncData.isRefreshing(previous)) if (AsyncData.isRefreshing(previous))
return AsyncData.refreshing(previous.previous) return AsyncData.refreshing(previous.previous)
return AsyncData.loading() return AsyncData.loading()
}).pipe( }).pipe(
Effect.andThen(latestKeyRef), Effect.andThen(run),
Effect.flatMap(identity),
Effect.flatMap(key => query(key).pipe(
Effect.matchCauseEffect({
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
})
)),
Effect.provide(context),
Effect.fork, Effect.fork,
) )),
),
Effect.flatMap(fiber => Effect.flatMap(fiber =>
Ref.set(fiberRef, Option.some(fiber)).pipe( Ref.set(fiberRef, Option.some(fiber)).pipe(
@@ -128,7 +132,7 @@ export const make = <K extends readonly unknown[], A, E, R>(
) )
return { return {
query, context,
latestKeyRef, latestKeyRef,
stateRef, stateRef,

View File

@@ -1,3 +1,5 @@
export * as ErrorHandler from "./ErrorHandler.js"
export * as QueryClient from "./QueryClient.js"
export * from "./QueryExtension.js" export * from "./QueryExtension.js"
export * as QueryRunner from "./QueryRunner.js" export * as QueryRunner from "./QueryRunner.js"
export * as QueryService from "./QueryService.js" export * as QueryService from "./QueryService.js"