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 692b2b6..5f0a2c0 100644 --- a/packages/extension-query/src/QueryClient.ts +++ b/packages/extension-query/src/QueryClient.ts @@ -1,47 +1,56 @@ 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: ErrorHandler.ErrorHandler +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> { +export interface ServiceResult extends Context.TagClass< + Self, + typeof id, + QueryClient +> { readonly Live: Layer.Layer< Self, never, - EH extends ErrorHandler.DefaultErrorHandler ? never : EH + 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 + 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 ?? ErrorHandler.DefaultErrorHandler) as Effect.Effect< - ErrorHandler.ErrorHandler, + (props?.ErrorHandler ?? QueryErrorHandler.DefaultQueryErrorHandler) as Effect.Effect< + QueryErrorHandler.QueryErrorHandler, never, - EH extends ErrorHandler.DefaultErrorHandler ? never : EH + EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH > ) )) 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..e81e960 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)), + QueryClient, ]).pipe( - Effect.flatMap(([state, errorHandler]) => state.set(AsyncData.loading()).pipe( + 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) @@ -65,8 +65,8 @@ export const make = ( )) const forkMutate = (...key: K) => Effect.all([ - Ref.make(AsyncData.noData>()), - Queue.unbounded>>(), + Ref.make(AsyncData.noData>()), + Queue.unbounded>>(), ]).pipe( Effect.flatMap(([stateRef, stateQueue]) => Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe( diff --git a/packages/extension-query/src/internal/QueryRunner.ts b/packages/extension-query/src/internal/QueryRunner.ts index bc659d3..a216764 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({ @@ -82,12 +82,12 @@ export const make = ( const run = Effect.all([ queryStateTag, - QueryClient.pipe(Effect.flatMap(client => client.ErrorHandler)), + QueryClient, ]).pipe( - Effect.flatMap(([state, errorHandler]) => latestKeyRef.pipe( + Effect.flatMap(([state, client]) => latestKeyRef.pipe( Effect.flatMap(identity), Effect.flatMap(key => query(key).pipe( - errorHandler.handle, + client.errorHandler.handle, Effect.matchCauseEffect({ onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe( Effect.tap(state.set) @@ -103,7 +103,7 @@ export const make = ( 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 +139,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))