diff --git a/packages/effect-fc/src/Query.ts b/packages/effect-fc/src/Query.ts index 183b51b..9d81175 100644 --- a/packages/effect-fc/src/Query.ts +++ b/packages/effect-fc/src/Query.ts @@ -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 * as QueryClient from "./QueryClient.js" +import { type Cause, type Context, type Duration, Effect, Fiber, identity, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect" +import * as QueryClient from "./QueryClient.js" import * as Result from "./Result.js" @@ -15,6 +15,8 @@ extends Pipeable.Pipeable { readonly f: (key: K) => Effect.Effect readonly initialProgress: P + readonly staleTime: Duration.DurationInput + readonly latestKey: Subscribable.Subscribable> readonly fiber: Subscribable.Subscribable>> readonly result: Subscribable.Subscribable> @@ -41,6 +43,8 @@ extends Pipeable.Class() implements Query { readonly f: (key: K) => Effect.Effect, readonly initialProgress: P, + readonly staleTime: Duration.DurationInput, + readonly latestKey: SubscriptionRef.SubscriptionRef>, readonly fiber: SubscriptionRef.SubscriptionRef>>, readonly result: SubscriptionRef.SubscriptionRef>, @@ -148,6 +152,7 @@ export declare namespace make { readonly key: Stream.Stream readonly f: (key: NoInfer) => Effect.Effect>> readonly initialProgress?: P + readonly staleTime?: Duration.DurationInput } } @@ -158,12 +163,16 @@ export const make = Effect.fnUntraced(function* > { + const client = yield* QueryClient.QueryClient + return new QueryImpl( yield* Effect.context>(), options.key, options.f as any, options.initialProgress as P, + options.staleTime ?? client.defaultStaleTime, + yield* SubscriptionRef.make(Option.none()), yield* SubscriptionRef.make(Option.none>()), yield* SubscriptionRef.make(Result.initial()), diff --git a/packages/effect-fc/src/QueryClient.ts b/packages/effect-fc/src/QueryClient.ts index d52adae..5862865 100644 --- a/packages/effect-fc/src/QueryClient.ts +++ b/packages/effect-fc/src/QueryClient.ts @@ -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 Result from "./Result.js" @@ -8,8 +8,9 @@ export type QueryClientServiceTypeId = typeof QueryClientServiceTypeId export interface QueryClientService extends Pipeable.Pipeable { readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId - readonly cache: Ref.Ref> - readonly defaultTtl: Duration.DurationInput + readonly cache: SubscriptionRef.SubscriptionRef> + readonly gcTime: Duration.DurationInput + readonly defaultStaleTime: Duration.DurationInput } export class QueryClient extends Effect.Service()("@effect-fc/QueryClient/QueryClient", { @@ -22,8 +23,10 @@ implements QueryClientService { readonly [QueryClientServiceTypeId]: QueryClientServiceTypeId = QueryClientServiceTypeId constructor( - readonly cache: Ref.Ref>, - readonly defaultTtl: Duration.DurationInput, + readonly cache: SubscriptionRef.SubscriptionRef>, + readonly gcTime: Duration.DurationInput, + readonly defaultStaleTime: Duration.DurationInput, + readonly runSemaphore: Effect.Semaphore, ) { super() } @@ -33,14 +36,17 @@ export const isQueryClientService = (u: unknown): u is QueryClientService => Pre export declare namespace make { 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 { return new QueryClientServiceImpl( - yield* Ref.make(HashMap.empty()), - options.defaultTtl ?? "5 minutes", + yield* SubscriptionRef.make(HashMap.empty()), + options.gcTime ?? "5 minutes", + options.defaultStaleTime ?? "0 minutes", + yield* Effect.makeSemaphore(1), ) }) @@ -63,6 +69,7 @@ export class QueryClientCacheKey extends Pipeable.Class() implements Pipeable.Pipeable, Equal.Equal { readonly [QueryClientCacheKeyTypeId]: QueryClientCacheKeyTypeId = QueryClientCacheKeyTypeId + constructor( readonly key: Query.Query.AnyKey, readonly f: (key: Query.Query.AnyKey) => Effect.Effect, @@ -90,9 +97,8 @@ implements Pipeable.Pipeable { readonly [QueryClientCacheEntryTypeId]: QueryClientCacheEntryTypeId = QueryClientCacheEntryTypeId constructor( - readonly at: DateTime.DateTime, - readonly ttl: Duration.DurationInput, readonly result: Result.Final, + readonly createdAt: DateTime.DateTime, ) { super() } diff --git a/packages/effect-fc/src/Result.ts b/packages/effect-fc/src/Result.ts index cdab4ff..d0ea148 100644 --- a/packages/effect-fc/src/Result.ts +++ b/packages/effect-fc/src/Result.ts @@ -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 type ResultTypeId = typeof ResultTypeId -export type Result = ( +export type Result = ResultState & (ResultState | Optimistic) +type ResultState = ( | Initial | Running

| Final ) export type Final = ( - | Success - | (Success & Refreshing

) - | Failure - | (Failure & Refreshing

) + & FinalState + & (FinalState | Refreshing

) + & (FinalState | Optimistic) ) +type FinalState = Success | Failure export namespace Result { export interface Prototype extends Pipeable.Pipeable, Equal.Equal { @@ -52,6 +53,10 @@ export interface Refreshing

{ readonly progress: P } +export interface Optimistic { + readonly optimistic: true +} + const ResultPrototype = Object.freeze({ ...Pipeable.Prototype, @@ -106,6 +111,7 @@ export const isRunning = (u: unknown): u is Running => isResult(u) && u export const isSuccess = (u: unknown): u is Success => isResult(u) && u._tag === "Success" export const isFailure = (u: unknown): u is Failure => isResult(u) && u._tag === "Failure" export const isRefreshing = (u: unknown): u is Refreshing => 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: { (): Initial @@ -131,6 +137,13 @@ export const refreshing = | Failure, P = never Object.getPrototypeOf(result), ) +export const optimistic = | Failure>( + result: R +): Omit & Optimistic => Object.setPrototypeOf( + Object.assign({}, result, { optimistic: true }), + Object.getPrototypeOf(result), +) + export const fromExit = ( exit: Exit.Exit, previousSuccess?: Success>,