From 2a29f19ece9d530fbd970c8db3476d41b0a4b0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 26 May 2025 04:15:01 +0200 Subject: [PATCH] @reffuse/extension-query 0.1.4 (#15) 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/15 --- packages/example/src/main.tsx | 4 +- .../example/src/query/services/Uuid4Query.ts | 4 +- .../src/query/views/Uuid4QueryService.tsx | 2 +- packages/example/src/routes/query/service.tsx | 11 +- .../example/src/routes/query/usemutation.tsx | 9 +- .../example/src/routes/query/usequery.tsx | 9 +- packages/extension-query/package.json | 4 +- .../src/{internal => }/MutationRunner.ts | 45 ++-- .../extension-query/src/MutationService.ts | 16 -- packages/extension-query/src/QueryClient.ts | 6 +- .../extension-query/src/QueryErrorHandler.ts | 4 +- .../extension-query/src/QueryExtension.ts | 79 +------ packages/extension-query/src/QueryProgress.ts | 2 +- packages/extension-query/src/QueryRunner.ts | 193 ++++++++++++++++++ packages/extension-query/src/QueryService.ts | 16 -- packages/extension-query/src/index.ts | 4 +- .../src/internal/QueryRunner.ts | 191 ----------------- .../extension-query/src/internal/index.ts | 2 - 18 files changed, 257 insertions(+), 344 deletions(-) rename packages/extension-query/src/{internal => }/MutationRunner.ts (66%) delete mode 100644 packages/extension-query/src/MutationService.ts create mode 100644 packages/extension-query/src/QueryRunner.ts delete mode 100644 packages/extension-query/src/QueryService.ts delete mode 100644 packages/extension-query/src/internal/QueryRunner.ts diff --git a/packages/example/src/main.tsx b/packages/example/src/main.tsx index a5da407..44742b5 100644 --- a/packages/example/src/main.tsx +++ b/packages/example/src/main.tsx @@ -11,8 +11,8 @@ import { routeTree } from "./routeTree.gen" const layer = Layer.empty.pipe( - Layer.provideMerge(AppQueryClient.Live), - Layer.provideMerge(AppQueryErrorHandler.Live), + Layer.provideMerge(AppQueryClient.Default), + Layer.provideMerge(AppQueryErrorHandler.Default), Layer.provideMerge(Clipboard.layer), Layer.provideMerge(Geolocation.layer), Layer.provideMerge(Permissions.layer), diff --git a/packages/example/src/query/services/Uuid4Query.ts b/packages/example/src/query/services/Uuid4Query.ts index 30b1545..46708bf 100644 --- a/packages/example/src/query/services/Uuid4Query.ts +++ b/packages/example/src/query/services/Uuid4Query.ts @@ -1,10 +1,10 @@ -import { QueryService } from "@reffuse/extension-query" +import { QueryRunner } from "@reffuse/extension-query" import { ParseResult, Schema } from "effect" export const Result = Schema.Array(Schema.String) -export class Uuid4Query extends QueryService.Tag("Uuid4Query") Uuid4Query.Uuid4Query, []) - const [state] = R.useRefState(query.state) + const [state] = R.useSubscribeRefs(query.stateRef) return ( diff --git a/packages/example/src/routes/query/service.tsx b/packages/example/src/routes/query/service.tsx index 8211667..8e15e40 100644 --- a/packages/example/src/routes/query/service.tsx +++ b/packages/example/src/routes/query/service.tsx @@ -4,7 +4,7 @@ import { Uuid4QueryService } from "@/query/views/Uuid4QueryService" import { R } from "@/reffuse" import { HttpClient } from "@effect/platform" import { createFileRoute } from "@tanstack/react-router" -import { Console, Effect, Schema } from "effect" +import { Console, Effect, Layer, Schema } from "effect" import { useMemo } from "react" @@ -17,15 +17,18 @@ function RouteComponent() { key: R.useStreamFromReactiveValues(["uuid4", 10 as number]), query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe( Effect.andThen(Effect.sleep("500 millis")), - Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)), - HttpClient.withTracerPropagation(false), + Effect.andThen(Effect.map( + HttpClient.HttpClient, + HttpClient.withTracerPropagation(false), + )), + Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)), Effect.flatMap(res => res.json), Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)), Effect.scoped, ), }) - const layer = useMemo(() => query.layer(Uuid4Query.Uuid4Query), [query]) + const layer = useMemo(() => Layer.succeed(Uuid4Query.Uuid4Query, query), [query]) return ( diff --git a/packages/example/src/routes/query/usemutation.tsx b/packages/example/src/routes/query/usemutation.tsx index fdc042a..6a3ff66 100644 --- a/packages/example/src/routes/query/usemutation.tsx +++ b/packages/example/src/routes/query/usemutation.tsx @@ -29,15 +29,18 @@ function RouteComponent() { Effect.tap(() => QueryProgress.QueryProgress.update(() => AsyncData.Progress.make({ loaded: 50, total: Option.some(100) }) )), - Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)), - HttpClient.withTracerPropagation(false), + Effect.andThen(Effect.map( + HttpClient.HttpClient, + HttpClient.withTracerPropagation(false), + )), + Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)), Effect.flatMap(res => res.json), Effect.flatMap(Schema.decodeUnknown(Result)), Effect.scoped, ) }) - const [state] = R.useSubscribeRefs(mutation.state) + const [state] = R.useSubscribeRefs(mutation.stateRef) return ( diff --git a/packages/example/src/routes/query/usequery.tsx b/packages/example/src/routes/query/usequery.tsx index 4782a49..4afc21f 100644 --- a/packages/example/src/routes/query/usequery.tsx +++ b/packages/example/src/routes/query/usequery.tsx @@ -23,15 +23,18 @@ function RouteComponent() { key: R.useStreamFromReactiveValues(["uuid4", count]), query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe( Effect.andThen(Effect.sleep("500 millis")), - Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)), - HttpClient.withTracerPropagation(false), + Effect.andThen(Effect.map( + HttpClient.HttpClient, + HttpClient.withTracerPropagation(false), + )), + Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)), Effect.flatMap(res => res.json), Effect.flatMap(Schema.decodeUnknown(Result)), Effect.scoped, ), }) - const [state] = R.useSubscribeRefs(query.state) + const [state] = R.useSubscribeRefs(query.stateRef) return ( diff --git a/packages/extension-query/package.json b/packages/extension-query/package.json index ec67d63..11f96ff 100644 --- a/packages/extension-query/package.json +++ b/packages/extension-query/package.json @@ -1,6 +1,6 @@ { "name": "@reffuse/extension-query", - "version": "0.1.3", + "version": "0.1.4", "type": "module", "files": [ "./README.md", @@ -39,6 +39,6 @@ "@types/react": "^19.0.0", "effect": "^3.15.0", "react": "^19.0.0", - "reffuse": "^0.1.6" + "reffuse": "^0.1.11" } } diff --git a/packages/extension-query/src/internal/MutationRunner.ts b/packages/extension-query/src/MutationRunner.ts similarity index 66% rename from packages/extension-query/src/internal/MutationRunner.ts rename to packages/extension-query/src/MutationRunner.ts index 3865223..d25bcea 100644 --- a/packages/extension-query/src/internal/MutationRunner.ts +++ b/packages/extension-query/src/MutationRunner.ts @@ -1,12 +1,11 @@ import * as AsyncData from "@typed/async-data" -import { type Context, Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect" -import type * as QueryClient from "../QueryClient.js" -import * as QueryProgress from "../QueryProgress.js" -import * as QueryState from "./QueryState.js" +import { Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect" +import type * as QueryClient from "./QueryClient.js" +import * as QueryProgress from "./QueryProgress.js" +import { QueryState } from "./internal/index.js" -export interface MutationRunner { - readonly context: Context.Context +export interface MutationRunner { readonly stateRef: SubscriptionRef.SubscriptionRef> readonly mutate: (...key: K) => Effect.Effect | AsyncData.Failure> @@ -17,6 +16,11 @@ export interface MutationRunner { } +export const Tag = (id: Id) => < + Self, K extends readonly unknown[], A, E = never, +>() => Effect.Tag(id)>() + + export interface MakeProps { readonly QueryClient: QueryClient.GenericTagClass readonly mutation: (key: K) => Effect.Effect @@ -28,7 +32,7 @@ export const make = mutation, }: MakeProps ): Effect.Effect< - MutationRunner, R>, + MutationRunner>, never, R | QueryClient.TagClassShape > => Effect.gen(function*() { @@ -37,25 +41,18 @@ export const make = const queryStateTag = QueryState.makeTag>() - const run = (key: K) => Effect.Do.pipe( - Effect.bind("state", () => queryStateTag), - Effect.bind("client", () => QueryClient), - - Effect.flatMap(({ state, client }) => state.set(AsyncData.loading()).pipe( + const run = (key: K) => Effect.all([QueryClient, queryStateTag]).pipe( + Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe( Effect.andThen(mutation(key)), 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) - ), + onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set), + onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set), }), )), Effect.provide(context), - Effect.provide(QueryProgress.QueryProgress.Live), + Effect.provide(QueryProgress.QueryProgress.Default), ) const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer( @@ -64,11 +61,11 @@ export const make = value => Ref.set(globalStateRef, value), )) - const forkMutate = (...key: K) => Effect.Do.pipe( - Effect.bind("stateRef", () => Ref.make(AsyncData.noData>())), - Effect.bind("stateQueue", () => Queue.unbounded>>()), - - Effect.flatMap(({ stateRef, stateQueue }) => + const forkMutate = (...key: K) => Effect.all([ + Ref.make(AsyncData.noData>()), + Queue.unbounded>>(), + ]).pipe( + Effect.flatMap(([stateRef, stateQueue]) => Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe( Effect.andThen(run(key)), Effect.scoped, diff --git a/packages/extension-query/src/MutationService.ts b/packages/extension-query/src/MutationService.ts deleted file mode 100644 index bdfb30a..0000000 --- a/packages/extension-query/src/MutationService.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type * as AsyncData from "@typed/async-data" -import { Effect, type Fiber, type Stream, type SubscriptionRef } from "effect" - - -export interface MutationService { - readonly state: SubscriptionRef.SubscriptionRef> - readonly mutate: (...key: K) => Effect.Effect | AsyncData.Failure> - readonly forkMutate: (...key: K) => Effect.Effect | AsyncData.Failure>, - state: Stream.Stream>, - ]> -} - -export const Tag = (id: Id) => < - Self, K extends readonly unknown[], A, E = never, ->() => Effect.Tag(id)>() diff --git a/packages/extension-query/src/QueryClient.ts b/packages/extension-query/src/QueryClient.ts index 2b9b003..1e638f6 100644 --- a/packages/extension-query/src/QueryClient.ts +++ b/packages/extension-query/src/QueryClient.ts @@ -28,7 +28,7 @@ export interface ServiceResult extends Context.Ta typeof id, QueryClient > { - readonly Live: Layer.Layer< + readonly Default: Layer.Layer< Self | (EH extends QueryErrorHandler.DefaultQueryErrorHandler ? EH : never), never, EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH @@ -45,7 +45,7 @@ export const Service = () => ( ): ServiceResult => { const TagClass = Context.Tag(id)() as ServiceResult - (TagClass as Mutable).Live = Layer.effect(TagClass, Effect.Do.pipe( + (TagClass as Mutable).Default = Layer.effect(TagClass, Effect.Do.pipe( Effect.bind("errorHandler", () => (props?.ErrorHandler ?? QueryErrorHandler.DefaultQueryErrorHandler) as Effect.Effect< QueryErrorHandler.QueryErrorHandler, @@ -56,7 +56,7 @@ export const Service = () => ( )).pipe( Layer.provideMerge((props?.ErrorHandler ? Layer.empty - : QueryErrorHandler.DefaultQueryErrorHandler.Live + : QueryErrorHandler.DefaultQueryErrorHandler.Default ) as Layer.Layer) ) diff --git a/packages/extension-query/src/QueryErrorHandler.ts b/packages/extension-query/src/QueryErrorHandler.ts index da64787..ebfa750 100644 --- a/packages/extension-query/src/QueryErrorHandler.ts +++ b/packages/extension-query/src/QueryErrorHandler.ts @@ -21,7 +21,7 @@ export interface ServiceResult< Id, QueryErrorHandler > { - readonly Live: Layer.Layer + readonly Default: Layer.Layer } export const Service = () => ( @@ -35,7 +35,7 @@ export const Service = () => ( ): ServiceResult => { const TagClass = Context.Tag(id)() as ServiceResult - (TagClass as Mutable).Live = Layer.effect(TagClass, Effect.gen(function*() { + (TagClass as Mutable).Default = Layer.effect(TagClass, Effect.gen(function*() { const pubsub = yield* PubSub.unbounded>() const errors = Stream.fromPubSub(pubsub) diff --git a/packages/extension-query/src/QueryExtension.ts b/packages/extension-query/src/QueryExtension.ts index ddd6b07..0e46e40 100644 --- a/packages/extension-query/src/QueryExtension.ts +++ b/packages/extension-query/src/QueryExtension.ts @@ -1,53 +1,21 @@ -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 * as React from "react" +import type { Effect, Stream } from "effect" import { ReffuseExtension, type ReffuseNamespace } from "reffuse" -import type * as MutationService from "./MutationService.js" +import * as MutationRunner from "./MutationRunner.js" import * as QueryClient from "./QueryClient.js" import type * as QueryProgress from "./QueryProgress.js" -import type * as QueryService from "./QueryService.js" -import { MutationRunner, QueryRunner } from "./internal/index.js" +import * as QueryRunner from "./QueryRunner.js" export interface UseQueryProps { readonly key: Stream.Stream readonly query: (key: K) => Effect.Effect - readonly refreshOnWindowFocus?: boolean + readonly options?: QueryRunner.RunOptions } -export interface UseQueryResult { - readonly latestKey: SubscriptionRef.SubscriptionRef> - readonly state: SubscriptionRef.SubscriptionRef> - - readonly forkRefresh: Effect.Effect | AsyncData.Failure, Cause.NoSuchElementException>, - state: Stream.Stream>, - ]> - - readonly layer: ( - tag: Context.TagClass> - ) => Layer.Layer -} - - export interface UseMutationProps { readonly mutation: (key: K) => Effect.Effect } -export interface UseMutationResult { - readonly state: SubscriptionRef.SubscriptionRef> - - readonly mutate: (...key: K) => Effect.Effect | AsyncData.Failure> - readonly forkMutate: (...key: K) => Effect.Effect | AsyncData.Failure>, - state: Stream.Stream>, - ]> - - readonly layer: ( - tag: Context.TagClass> - ) => Layer.Layer -} - export const QueryExtension = ReffuseExtension.make(() => ({ useQuery< @@ -61,32 +29,16 @@ export const QueryExtension = ReffuseExtension.make(() => ({ >( this: ReffuseNamespace.ReffuseNamespace>, props: UseQueryProps, - ): UseQueryResult> { + ): QueryRunner.QueryRunner> { const runner = this.useMemo(() => QueryRunner.make({ QueryClient: QueryClient.makeGenericTagClass(), key: props.key, query: props.query, }), [props.key]) - this.useFork(() => runner.fetchOnKeyChange, [runner]) + this.useFork(() => QueryRunner.run(runner, props.options), [runner]) - this.useFork(() => (props.refreshOnWindowFocus ?? true) - ? runner.refreshOnWindowFocus - : Effect.void, - [props.refreshOnWindowFocus, runner]) - - return React.useMemo(() => ({ - latestKey: runner.latestKeyRef, - state: runner.stateRef, - - forkRefresh: runner.forkRefresh, - - layer: tag => Layer.succeed(tag, { - latestKey: runner.latestKeyRef, - state: runner.stateRef, - forkRefresh: runner.forkRefresh, - }), - }), [runner]) + return runner }, useMutation< @@ -100,23 +52,10 @@ export const QueryExtension = ReffuseExtension.make(() => ({ >( this: ReffuseNamespace.ReffuseNamespace>, props: UseMutationProps, - ): UseMutationResult> { - const runner = this.useMemo(() => MutationRunner.make({ + ): MutationRunner.MutationRunner> { + return this.useMemo(() => MutationRunner.make({ QueryClient: QueryClient.makeGenericTagClass(), mutation: props.mutation, }), []) - - return React.useMemo(() => ({ - state: runner.stateRef, - - mutate: runner.mutate, - forkMutate: runner.forkMutate, - - layer: tag => Layer.succeed(tag, { - state: runner.stateRef, - mutate: runner.mutate, - forkMutate: runner.forkMutate, - }), - }), [runner]) }, })) diff --git a/packages/extension-query/src/QueryProgress.ts b/packages/extension-query/src/QueryProgress.ts index 4a78ec5..ba8288e 100644 --- a/packages/extension-query/src/QueryProgress.ts +++ b/packages/extension-query/src/QueryProgress.ts @@ -10,7 +10,7 @@ export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryPro f: (previous: Option.Option) => AsyncData.Progress ) => Effect.Effect }>() { - static readonly Live: Layer.Layer< + static readonly Default: Layer.Layer< QueryProgress, never, QueryState.QueryState diff --git a/packages/extension-query/src/QueryRunner.ts b/packages/extension-query/src/QueryRunner.ts new file mode 100644 index 0000000..c8754fd --- /dev/null +++ b/packages/extension-query/src/QueryRunner.ts @@ -0,0 +1,193 @@ +import { BrowserStream } from "@effect/platform-browser" +import * as AsyncData from "@typed/async-data" +import { type Cause, 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 { QueryState } from "./internal/index.js" + + +export interface QueryRunner { + readonly queryKey: Stream.Stream + readonly latestKeyValueRef: SubscriptionRef.SubscriptionRef> + readonly stateRef: SubscriptionRef.SubscriptionRef> + readonly fiberRef: SubscriptionRef.SubscriptionRef | AsyncData.Failure, + Cause.NoSuchElementException + >>> + + readonly interrupt: Effect.Effect + readonly forkInterrupt: Effect.Effect> + readonly forkFetch: (keyValue: K) => Effect.Effect | AsyncData.Failure>, + state: Stream.Stream>, + ]> + readonly forkRefresh: Effect.Effect | AsyncData.Failure, Cause.NoSuchElementException>, + state: Stream.Stream>, + ]> +} + + +export const Tag = (id: Id) => < + Self, K extends readonly unknown[], A, E = never +>() => Effect.Tag(id)>() + + +export interface MakeProps { + readonly QueryClient: QueryClient.GenericTagClass + readonly key: Stream.Stream + readonly query: (key: K) => Effect.Effect +} + +export const make = ( + { + QueryClient, + key, + query, + }: MakeProps +): Effect.Effect< + QueryRunner>, + never, + R | QueryClient.TagClassShape +> => Effect.gen(function*() { + const context = yield* Effect.context>() + + const latestKeyValueRef = yield* SubscriptionRef.make(Option.none()) + const stateRef = yield* SubscriptionRef.make(AsyncData.noData>()) + const fiberRef = yield* SubscriptionRef.make(Option.none | AsyncData.Failure>, + Cause.NoSuchElementException + >>()) + + const queryStateTag = QueryState.makeTag>() + + const interrupt = Effect.flatMap(fiberRef, Option.match({ + onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( + Effect.andThen(Fiber.interrupt(fiber)) + ), + onNone: () => Effect.void, + })) + + const forkInterrupt = Effect.flatMap(fiberRef, Option.match({ + onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( + Effect.andThen(Fiber.interrupt(fiber).pipe( + Effect.asVoid, + Effect.forkDaemon, + )) + ), + onNone: () => Effect.forkDaemon(Effect.void), + })) + + const run = (keyValue: K) => Effect.all([QueryClient, queryStateTag]).pipe( + Effect.flatMap(([client, state]) => Ref.set(latestKeyValueRef, Option.some(keyValue)).pipe( + Effect.andThen(query(keyValue)), + client.errorHandler.handle, + Effect.matchCauseEffect({ + onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set), + onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set), + }), + )), + + Effect.provide(context), + Effect.provide(QueryProgress.QueryProgress.Default), + ) + + const forkFetch = (keyValue: K) => Queue.unbounded>>().pipe( + Effect.flatMap(stateQueue => queryStateTag.pipe( + Effect.flatMap(state => interrupt.pipe( + Effect.andThen( + Effect.addFinalizer(() => Effect.andThen( + Ref.set(fiberRef, Option.none()), + Queue.shutdown(stateQueue), + )).pipe( + Effect.andThen(state.set(AsyncData.loading())), + Effect.andThen(run(keyValue)), + Effect.scoped, + Effect.forkDaemon, + ) + ), + + Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))), + Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const), + )), + + Effect.provide(QueryState.layer( + queryStateTag, + stateRef, + value => Effect.andThen( + Queue.offer(stateQueue, value), + Ref.set(stateRef, value), + ), + )), + )) + ) + + const setInitialRefreshState = Effect.flatMap(queryStateTag, 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( + Effect.addFinalizer(() => Effect.andThen( + Ref.set(fiberRef, Option.none()), + Queue.shutdown(stateQueue), + )).pipe( + Effect.andThen(setInitialRefreshState), + Effect.andThen(latestKeyValueRef.pipe( + Effect.flatMap(identity), + Effect.flatMap(run), + )), + Effect.scoped, + Effect.forkDaemon, + ) + ), + + Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))), + Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const), + + Effect.provide(QueryState.layer( + queryStateTag, + stateRef, + value => Effect.andThen( + Queue.offer(stateQueue, value), + Ref.set(stateRef, value), + ), + )), + )) + ) + + return { + queryKey: key, + latestKeyValueRef, + stateRef, + fiberRef, + + interrupt, + forkInterrupt, + forkFetch, + forkRefresh, + } +}) + + +export interface RunOptions { + readonly refreshOnWindowFocus?: boolean +} + +export const run = ( + self: QueryRunner, + options?: RunOptions, +): Effect.Effect => Effect.gen(function*() { + if (typeof window !== "undefined" && (options?.refreshOnWindowFocus ?? true)) + yield* Effect.forkScoped( + Stream.runForEach(BrowserStream.fromEventListenerWindow("focus"), () => self.forkRefresh) + ) + + yield* Effect.addFinalizer(() => self.interrupt) + yield* Stream.runForEach(Stream.changes(self.queryKey), latestKey => self.forkFetch(latestKey)) +}) diff --git a/packages/extension-query/src/QueryService.ts b/packages/extension-query/src/QueryService.ts deleted file mode 100644 index ef447b9..0000000 --- a/packages/extension-query/src/QueryService.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type * as AsyncData from "@typed/async-data" -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 forkRefresh: Effect.Effect | AsyncData.Failure, Cause.NoSuchElementException>, - state: Stream.Stream>, - ]> -} - -export const Tag = (id: Id) => < - Self, K extends readonly unknown[], A, E = never, ->() => Effect.Tag(id)>() diff --git a/packages/extension-query/src/index.ts b/packages/extension-query/src/index.ts index 4f0feaf..71e9d7a 100644 --- a/packages/extension-query/src/index.ts +++ b/packages/extension-query/src/index.ts @@ -1,6 +1,6 @@ -export * as MutationService from "./MutationService.js" +export * as MutationRunner from "./MutationRunner.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" +export * as QueryRunner from "./QueryRunner.js" diff --git a/packages/extension-query/src/internal/QueryRunner.ts b/packages/extension-query/src/internal/QueryRunner.ts deleted file mode 100644 index 5b8bab7..0000000 --- a/packages/extension-query/src/internal/QueryRunner.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { BrowserStream } from "@effect/platform-browser" -import * as AsyncData from "@typed/async-data" -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" - - -export interface QueryRunner { - readonly context: Context.Context - - readonly latestKeyRef: SubscriptionRef.SubscriptionRef> - readonly stateRef: SubscriptionRef.SubscriptionRef> - readonly fiberRef: SubscriptionRef.SubscriptionRef | AsyncData.Failure, - Cause.NoSuchElementException - >>> - - readonly forkInterrupt: 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 -} - - -export interface MakeProps { - readonly QueryClient: QueryClient.GenericTagClass - readonly key: Stream.Stream - readonly query: (key: K) => Effect.Effect -} - -export const make = ( - { - QueryClient, - key, - query, - }: MakeProps -): Effect.Effect< - QueryRunner, R>, - never, - R | QueryClient.TagClassShape -> => Effect.gen(function*() { - const context = yield* Effect.context>() - - const latestKeyRef = yield* SubscriptionRef.make(Option.none()) - const stateRef = yield* SubscriptionRef.make(AsyncData.noData>()) - const fiberRef = yield* SubscriptionRef.make(Option.none | AsyncData.Failure>, - Cause.NoSuchElementException - >>()) - - const queryStateTag = QueryState.makeTag>() - - const interrupt = fiberRef.pipe( - Effect.flatMap(Option.match({ - onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( - Effect.andThen(Fiber.interrupt(fiber)) - ), - onNone: () => Effect.void, - })) - ) - - const forkInterrupt = fiberRef.pipe( - Effect.flatMap(Option.match({ - onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( - Effect.andThen(Fiber.interrupt(fiber).pipe( - Effect.asVoid, - Effect.forkDaemon, - )) - ), - onNone: () => Effect.forkDaemon(Effect.void), - })) - ) - - 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( - Effect.flatMap(stateQueue => queryStateTag.pipe( - Effect.flatMap(state => interrupt.pipe( - Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe( - Effect.andThen(Queue.shutdown(stateQueue)) - )).pipe( - Effect.andThen(state.set(AsyncData.loading())), - Effect.andThen(run), - Effect.scoped, - Effect.forkDaemon, - )), - - Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))), - Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const), - )), - - Effect.provide(QueryState.layer( - queryStateTag, - stateRef, - value => Queue.offer(stateQueue, value).pipe( - Effect.andThen(Ref.set(stateRef, value)) - ), - )), - )) - ) - - 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(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe( - Effect.andThen(Queue.shutdown(stateQueue)) - )).pipe( - Effect.andThen(setInitialRefreshState), - Effect.andThen(run), - Effect.scoped, - Effect.forkDaemon, - )), - - Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))), - Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const), - - Effect.provide(QueryState.layer( - queryStateTag, - stateRef, - value => Queue.offer(stateQueue, value).pipe( - Effect.andThen(Ref.set(stateRef, value)) - ), - )), - )) - ) - - const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe( - Effect.andThen(Stream.runForEach(Stream.changes(key), latestKey => - Ref.set(latestKeyRef, Option.some(latestKey)).pipe( - Effect.andThen(forkFetch) - ) - )) - ) - - const refreshOnWindowFocus = Stream.runForEach( - BrowserStream.fromEventListenerWindow("focus"), - () => forkRefresh, - ) - - return { - context, - - latestKeyRef, - stateRef, - fiberRef, - - forkInterrupt, - forkFetch, - forkRefresh, - - fetchOnKeyChange, - refreshOnWindowFocus, - } -}) diff --git a/packages/extension-query/src/internal/index.ts b/packages/extension-query/src/internal/index.ts index 425849b..d062e02 100644 --- a/packages/extension-query/src/internal/index.ts +++ b/packages/extension-query/src/internal/index.ts @@ -1,3 +1 @@ -export * as MutationRunner from "./MutationRunner.js" -export * as QueryRunner from "./QueryRunner.js" export * as QueryState from "./QueryState.js"