QueryClient work
All checks were successful
Lint / lint (push) Successful in 58s

This commit is contained in:
Julien Valverdé
2025-12-18 04:31:35 +01:00
parent f09c6e3a1c
commit cfd3028218
3 changed files with 45 additions and 17 deletions

View File

@@ -1,5 +1,5 @@
import { type Cause, type Context, Effect, Fiber, identity, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect" import { type Cause, type Context, type Duration, Effect, Fiber, identity, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
import type * as QueryClient from "./QueryClient.js" import * as QueryClient from "./QueryClient.js"
import * as Result from "./Result.js" import * as Result from "./Result.js"
@@ -15,6 +15,8 @@ extends Pipeable.Pipeable {
readonly f: (key: K) => Effect.Effect<A, E, R> readonly f: (key: K) => Effect.Effect<A, E, R>
readonly initialProgress: P readonly initialProgress: P
readonly staleTime: Duration.DurationInput
readonly latestKey: Subscribable.Subscribable<Option.Option<K>> readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>> readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>> readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
@@ -41,6 +43,8 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
readonly f: (key: K) => Effect.Effect<A, E, R>, readonly f: (key: K) => Effect.Effect<A, E, R>,
readonly initialProgress: P, readonly initialProgress: P,
readonly staleTime: Duration.DurationInput,
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>, readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>, readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>, readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
@@ -148,6 +152,7 @@ export declare namespace make {
readonly key: Stream.Stream<K> readonly key: Stream.Stream<K>
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>> readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
readonly initialProgress?: P readonly initialProgress?: P
readonly staleTime?: Duration.DurationInput
} }
} }
@@ -158,12 +163,16 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, E =
never, never,
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P> Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
> { > {
const client = yield* QueryClient.QueryClient
return new QueryImpl( return new QueryImpl(
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>>(), yield* Effect.context<Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>>(),
options.key, options.key,
options.f as any, options.f as any,
options.initialProgress as P, options.initialProgress as P,
options.staleTime ?? client.defaultStaleTime,
yield* SubscriptionRef.make(Option.none<K>()), yield* SubscriptionRef.make(Option.none<K>()),
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()), yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
yield* SubscriptionRef.make(Result.initial<A, E, P>()), yield* SubscriptionRef.make(Result.initial<A, E, P>()),

View File

@@ -1,4 +1,4 @@
import { type DateTime, type Duration, Effect, Equal, Equivalence, Hash, HashMap, Pipeable, Predicate, Ref, type Scope } from "effect" import { type DateTime, type Duration, Effect, Equal, Equivalence, Hash, HashMap, Pipeable, Predicate, type Scope, SubscriptionRef } from "effect"
import type * as Query from "./Query.js" import type * as Query from "./Query.js"
import type * as Result from "./Result.js" import type * as Result from "./Result.js"
@@ -8,8 +8,9 @@ export type QueryClientServiceTypeId = typeof QueryClientServiceTypeId
export interface QueryClientService extends Pipeable.Pipeable { export interface QueryClientService extends Pipeable.Pipeable {
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId
readonly cache: Ref.Ref<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>> readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
readonly defaultTtl: Duration.DurationInput readonly gcTime: Duration.DurationInput
readonly defaultStaleTime: Duration.DurationInput
} }
export class QueryClient extends Effect.Service<QueryClient>()("@effect-fc/QueryClient/QueryClient", { export class QueryClient extends Effect.Service<QueryClient>()("@effect-fc/QueryClient/QueryClient", {
@@ -22,8 +23,10 @@ implements QueryClientService {
readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId = QueryClientServiceTypeId readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId = QueryClientServiceTypeId
constructor( constructor(
readonly cache: Ref.Ref<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>, readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>,
readonly defaultTtl: Duration.DurationInput, readonly gcTime: Duration.DurationInput,
readonly defaultStaleTime: Duration.DurationInput,
readonly runSemaphore: Effect.Semaphore,
) { ) {
super() super()
} }
@@ -33,14 +36,17 @@ export const isQueryClientService = (u: unknown): u is QueryClientService => Pre
export declare namespace make { export declare namespace make {
export interface Options { export interface Options {
readonly defaultTtl?: Duration.DurationInput readonly gcTime?: Duration.DurationInput
readonly defaultStaleTime?: Duration.DurationInput
} }
} }
export const make = Effect.fnUntraced(function* (options: make.Options = {}): Effect.fn.Return<QueryClientService> { export const make = Effect.fnUntraced(function* (options: make.Options = {}): Effect.fn.Return<QueryClientService> {
return new QueryClientServiceImpl( return new QueryClientServiceImpl(
yield* Ref.make(HashMap.empty<QueryClientCacheKey, QueryClientCacheEntry>()), yield* SubscriptionRef.make(HashMap.empty<QueryClientCacheKey, QueryClientCacheEntry>()),
options.defaultTtl ?? "5 minutes", options.gcTime ?? "5 minutes",
options.defaultStaleTime ?? "0 minutes",
yield* Effect.makeSemaphore(1),
) )
}) })
@@ -63,6 +69,7 @@ export class QueryClientCacheKey
extends Pipeable.Class() extends Pipeable.Class()
implements Pipeable.Pipeable, Equal.Equal { implements Pipeable.Pipeable, Equal.Equal {
readonly [QueryClientCacheKeyTypeId]: QueryClientCacheKeyTypeId = QueryClientCacheKeyTypeId readonly [QueryClientCacheKeyTypeId]: QueryClientCacheKeyTypeId = QueryClientCacheKeyTypeId
constructor( constructor(
readonly key: Query.Query.AnyKey, readonly key: Query.Query.AnyKey,
readonly f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>, readonly f: (key: Query.Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>,
@@ -90,9 +97,8 @@ implements Pipeable.Pipeable {
readonly [QueryClientCacheEntryTypeId]: QueryClientCacheEntryTypeId = QueryClientCacheEntryTypeId readonly [QueryClientCacheEntryTypeId]: QueryClientCacheEntryTypeId = QueryClientCacheEntryTypeId
constructor( constructor(
readonly at: DateTime.DateTime,
readonly ttl: Duration.DurationInput,
readonly result: Result.Final<unknown, unknown, unknown>, readonly result: Result.Final<unknown, unknown, unknown>,
readonly createdAt: DateTime.DateTime,
) { ) {
super() super()
} }

View File

@@ -4,18 +4,19 @@ import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Mat
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result") export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
export type ResultTypeId = typeof ResultTypeId export type ResultTypeId = typeof ResultTypeId
export type Result<A, E = never, P = never> = ( export type Result<A, E = never, P = never> = ResultState<A, E, P> & (ResultState<A, E, P> | Optimistic)
type ResultState<A, E = never, P = never> = (
| Initial | Initial
| Running<P> | Running<P>
| Final<A, E, P> | Final<A, E, P>
) )
export type Final<A, E = never, P = never> = ( export type Final<A, E = never, P = never> = (
| Success<A> & FinalState<A, E>
| (Success<A> & Refreshing<P>) & (FinalState<A, E> | Refreshing<P>)
| Failure<A, E> & (FinalState<A, E> | Optimistic)
| (Failure<A, E> & Refreshing<P>)
) )
type FinalState<A, E = never> = Success<A> | Failure<A, E>
export namespace Result { export namespace Result {
export interface Prototype extends Pipeable.Pipeable, Equal.Equal { export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
@@ -52,6 +53,10 @@ export interface Refreshing<P = never> {
readonly progress: P readonly progress: P
} }
export interface Optimistic {
readonly optimistic: true
}
const ResultPrototype = Object.freeze({ const ResultPrototype = Object.freeze({
...Pipeable.Prototype, ...Pipeable.Prototype,
@@ -106,6 +111,7 @@ export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success" export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
export const isFailure = (u: unknown): u is Failure<unknown, unknown> => isResult(u) && u._tag === "Failure" export const isFailure = (u: unknown): u is Failure<unknown, unknown> => isResult(u) && u._tag === "Failure"
export const isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing") && u.refreshing export const isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing") && u.refreshing
export const isOptimistic = (u: unknown): u is Optimistic => isResult(u) && Predicate.hasProperty(u, "optimistic") && u.optimistic
export const initial: { export const initial: {
(): Initial (): Initial
@@ -131,6 +137,13 @@ export const refreshing = <R extends Success<any> | Failure<any, any>, P = never
Object.getPrototypeOf(result), Object.getPrototypeOf(result),
) )
export const optimistic = <R extends Success<any> | Failure<any, any>>(
result: R
): Omit<R, keyof Optimistic> & Optimistic => Object.setPrototypeOf(
Object.assign({}, result, { optimistic: true }),
Object.getPrototypeOf(result),
)
export const fromExit = <A, E>( export const fromExit = <A, E>(
exit: Exit.Exit<A, E>, exit: Exit.Exit<A, E>,
previousSuccess?: Success<NoInfer<A>>, previousSuccess?: Success<NoInfer<A>>,