diff --git a/src/index.ts b/src/index.ts index 9636423..3b5c45f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ export * from "./lib/state/persistance/usePersistSignals" // utils export * from "./lib/utils/useUnmountObservable" -export * from "./lib/utils/retryBackoff" +export * from "./lib/utils/operators/retryBackoff" export * from "./lib/utils/useLiveRef" // higher helpers diff --git a/src/lib/queries/client/mutations/Mutation.ts b/src/lib/queries/client/mutations/Mutation.ts deleted file mode 100644 index a5502fd..0000000 --- a/src/lib/queries/client/mutations/Mutation.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { - identity, - map, - merge, - of, - tap, - switchMap, - Subject, - takeUntil, - concat, - toArray, - mergeMap, - Observable, - shareReplay, - takeWhile, - BehaviorSubject, - type ObservedValueOf, - NEVER, - share, - iif, - catchError -} from "rxjs" -import { retryOnError } from "../operators" -import { - type MutationState, - type MutationOptions, - type MutationMeta -} from "./types" -import { getDefaultMutationState } from "./defaultMutationState" -import { mergeResults } from "./operators" -import { type DefaultError } from "../types" -import { type MutationCache } from "./cache/MutationCache" -import { functionAsObservable } from "../utils/functionAsObservable" - -interface MutationConfig { - mutationCache: MutationCache - options: MutationOptions - defaultOptions?: MutationOptions - state?: MutationState -} - -export class Mutation< - TData = unknown, - TError = DefaultError, - TVariables = void, - TContext = unknown -> { - protected mutationCache: MutationCache - protected observerCount = new BehaviorSubject(0) - protected destroySubject = new Subject() - protected resetSubject = new Subject() - protected executeSubject = new Subject() - - public state: MutationState = - getDefaultMutationState() - - public state$: Observable - public options: MutationOptions - public observerCount$ = this.observerCount.asObservable() - - constructor({ - options, - mutationCache, - state - }: MutationConfig) { - this.options = options - this.mutationCache = mutationCache - this.state = state ?? this.state - - this.state$ = merge( - of(this.state), - this.executeSubject.pipe( - switchMap((variables) => this.createMutation(variables)), - tap((value) => { - this.state = { ...this.state, ...value } - }), - takeUntil(this.destroySubject) - ), - this.resetSubject.pipe( - map(() => - getDefaultMutationState() - ) - ), - NEVER - ).pipe( - /** - * refCount as true somewhat make NEVER complete when there are - * no more observers. I thought I should have to complete manually (which is - * why we still cancel the observable when we remove it from cache) - */ - shareReplay({ bufferSize: 1, refCount: true }), - takeUntil(this.destroySubject), - (source) => { - return new Observable>( - (observer) => { - this.observerCount.next(this.observerCount.getValue() + 1) - const sub = source.subscribe(observer) - - return () => { - this.observerCount.next(this.observerCount.getValue() - 1) - sub.unsubscribe() - } - } - ) - } - ) - } - - get meta(): MutationMeta | undefined { - return this.options.meta - } - - setOptions( - options?: MutationOptions - ): void { - this.options = { ...this.options, ...options } - } - - createMutation(variables: TVariables) { - type LocalState = MutationState - - const isPaused = this.state.isPaused - - const defaultFn = async () => - await Promise.reject(new Error("No mutationFn found")) - - const mutationFn = this.options.mutationFn ?? defaultFn - - const onCacheMutate$ = iif( - () => isPaused, - of(null), - functionAsObservable( - () => - this.mutationCache.config.onMutate?.( - variables, - this as Mutation - ) - ) - ) - - const onOptionMutate$ = iif( - () => isPaused, - of(this.state.context), - functionAsObservable( - // eslint-disable-next-line @typescript-eslint/promise-function-async - () => this.options.onMutate?.(variables) ?? undefined - ) - ) - - const onMutate$ = onCacheMutate$.pipe( - mergeMap(() => onOptionMutate$), - share() - ) - - type QueryState = Omit, "data"> & { - // add layer to allow undefined as mutation result - result?: { data: TData } - } - - const onError = (error: TError, context: TContext, attempt: number) => { - console.error(error) - - const onCacheError$ = functionAsObservable( - () => - this.mutationCache.config.onError?.< - TData, - TError, - TVariables, - TContext - >(error as Error, variables, context, this) - ) - - const onError$ = functionAsObservable( - () => this.options.onError?.(error, variables, context) - ) - - return concat(onCacheError$, onError$).pipe( - catchError(() => of(error)), - toArray(), - map( - (): QueryState => ({ - failureCount: attempt, - result: undefined, - error, - failureReason: error, - context, - status: "error" - }) - ) - ) - } - - const queryRunner$ = onMutate$.pipe( - switchMap((context) => { - const fn$ = - typeof mutationFn === "function" - ? // eslint-disable-next-line @typescript-eslint/promise-function-async - functionAsObservable(() => mutationFn(variables)) - : mutationFn - - return fn$.pipe( - map( - (data): QueryState => ({ - result: { - data - }, - error: null, - context - }) - ), - retryOnError({ - ...this.options, - caughtError: (attempt, error) => - of({ - failureCount: attempt, - failureReason: error - }), - catchError: (attempt, error) => - onError(error, context as TContext, attempt) - }), - takeWhile( - ({ result, error }) => - result?.data === undefined && error === undefined, - true - ) - ) - }) - ) - - const initState$ = of({ - ...this.state, - variables, - status: "pending", - isPaused: false, - failureCount: 0, - failureReason: null, - submittedAt: this.state.submittedAt ?? new Date().getTime() - } satisfies LocalState & Required>) - - const mutation$ = merge( - initState$, - onMutate$.pipe(map((context) => ({ context }))), - queryRunner$.pipe( - switchMap(({ result: mutationData, error, ...restState }) => { - if (!mutationData && !error) - return of({ - ...restState - }) - - const onCacheSuccess$ = error - ? of(null) - : functionAsObservable( - () => - this.mutationCache.config.onSuccess?.( - mutationData?.data, - variables, - restState.context, - this as Mutation - ) - ) - - const onSuccess$ = error - ? of(null) - : functionAsObservable( - () => - this.options.onSuccess?.( - mutationData?.data as TData, - variables, - restState.context - ) - ) - - // to pass as option from cache not here - const onCacheSettled$ = functionAsObservable( - () => - this.mutationCache.config.onSettled?.( - mutationData?.data, - error as any, - variables, - restState.context, - this as Mutation - ) - ) - - const onOptionSettled$ = functionAsObservable( - () => - this.options.onSettled?.( - mutationData?.data, - error as TError, - variables, - restState.context - ) - ) - - const onSettled$ = concat(onCacheSettled$, onOptionSettled$).pipe( - catchError((error) => (mutationData ? of(mutationData) : of(error))) - ) - - const result$ = concat(onCacheSuccess$, onSuccess$, onSettled$).pipe( - toArray(), - map(() => - error - ? ({ - error, - data: undefined, - variables, - ...restState - } satisfies Partial) - : ({ - status: "success" as const, - error, - data: mutationData?.data, - variables, - ...restState - } satisfies Partial) - ), - catchError((error) => - onError(error, restState.context as TContext, 0) - ) - ) - - return result$ - }) - ) - ).pipe( - mergeResults, - (this.options.__queryRunnerHook as typeof identity) ?? identity, - takeUntil(this.destroySubject) - ) - - return mutation$ - } - - observeTillFinished() { - return this.state$.pipe( - takeWhile( - (result) => result.status !== "error" && result.status !== "success", - true - ) - ) - } - - /** - * @important - * The resulting observable will complete as soon as the mutation - * is over, unlike the state which can be re-subscribed later. - */ - execute(variables: TVariables) { - this.executeSubject.next(variables) - this.executeSubject.complete() - - return this.observeTillFinished() - } - - get destroyed$() { - return this.destroySubject.asObservable() - } - - continue() { - return this.execute(this.state.variables as TVariables) - } - - destroy() { - this.destroySubject.next() - this.destroySubject.complete() - this.executeSubject.complete() - } - - reset() { - this.resetSubject.next() - this.resetSubject.complete() - this.destroy() - } -} diff --git a/src/lib/queries/client/mutations/cache/MutationCache.ts b/src/lib/queries/client/mutations/cache/MutationCache.ts index c9931fb..7f1054a 100644 --- a/src/lib/queries/client/mutations/cache/MutationCache.ts +++ b/src/lib/queries/client/mutations/cache/MutationCache.ts @@ -1,5 +1,5 @@ import { type DefaultError } from "@tanstack/react-query" -import { Mutation } from "../Mutation" +import { Mutation } from "../mutation/Mutation" import { type QueryClient } from "../../createClient" import { type MutationFilters, diff --git a/src/lib/queries/client/mutations/cache/types.ts b/src/lib/queries/client/mutations/cache/types.ts index ecdc67c..2e8ba3e 100644 --- a/src/lib/queries/client/mutations/cache/types.ts +++ b/src/lib/queries/client/mutations/cache/types.ts @@ -1,4 +1,4 @@ -import { type Mutation } from "../Mutation" +import { type Mutation } from "../mutation/Mutation" import { type MutationObserver } from "../observers/MutationObserver" export type NotifyEventType = diff --git a/src/lib/queries/client/mutations/mutation/Mutation.ts b/src/lib/queries/client/mutations/mutation/Mutation.ts new file mode 100644 index 0000000..e4173fe --- /dev/null +++ b/src/lib/queries/client/mutations/mutation/Mutation.ts @@ -0,0 +1,163 @@ +import { + map, + merge, + of, + tap, + switchMap, + Subject, + takeUntil, + type Observable, + shareReplay, + takeWhile, + BehaviorSubject, + concat +} from "rxjs" +import { getDefaultMutationState } from "../defaultMutationState" +import { type DefaultError } from "../../types" +import { type MutationCache } from "../cache/MutationCache" +import { functionAsObservable } from "../../utils/functionAsObservable" +import { + type MutationState, + type MutationMeta, + type MutationOptions +} from "./types" +import { executeMutation } from "./executeMutation" +import { trackSubscriptions } from "../../../../utils/operators/trackSubscriptions" + +interface MutationConfig { + mutationCache: MutationCache + options: MutationOptions + defaultOptions?: MutationOptions + state?: MutationState +} + +export class Mutation< + TData = unknown, + TError = DefaultError, + TVariables = void, + TContext = unknown +> { + protected mutationCache: MutationCache + protected observerCount = new BehaviorSubject(0) + protected destroySubject = new Subject() + protected resetSubject = new Subject() + protected executeSubject = new Subject() + + public state: MutationState = + getDefaultMutationState() + + public state$: Observable + public options: MutationOptions + public observerCount$ = this.observerCount.asObservable() + public destroyed$ = this.destroySubject.asObservable() + + constructor({ + options, + mutationCache, + state + }: MutationConfig) { + this.options = options + this.mutationCache = mutationCache + this.state = state ?? this.state + + const initialState$ = of(this.state) + const resetState$ = this.resetSubject.pipe( + map(() => getDefaultMutationState()) + ) + const execution$ = this.executeSubject.pipe( + switchMap((variables) => + executeMutation({ + mutation: this, + mutationCache: this.mutationCache, + options: { + ...this.options, + // @todo test onError, onSettled + onSuccess: (data, variables, context) => { + const onCacheSuccess$ = functionAsObservable( + () => + mutationCache.config.onSuccess?.( + data, + variables, + context, + this as Mutation + ) + ) + + const onOptionSuccess$ = functionAsObservable( + () => this.options.onSuccess?.(data, variables, context) + ) + + return concat(onCacheSuccess$, onOptionSuccess$) + } + }, + state: this.state, + variables + }) + ), + tap((value) => { + this.state = { ...this.state, ...value } + }), + takeUntil(this.destroySubject) + ) + + this.state$ = merge(initialState$, execution$, resetState$).pipe( + /** + * refCount as true somewhat make NEVER complete when there are + * no more observers. I thought I should have to complete manually (which is + * why we still cancel the observable when we remove it from cache) + */ + shareReplay({ bufferSize: 1, refCount: true }), + takeUntil(this.destroySubject), + trackSubscriptions((count) => { + this.observerCount.next(count) + }) + ) + } + + get meta(): MutationMeta | undefined { + return this.options.meta + } + + setOptions( + options?: MutationOptions + ): void { + this.options = { ...this.options, ...options } + } + + observeTillFinished() { + return this.state$.pipe( + takeWhile( + (result) => result.status !== "error" && result.status !== "success", + true + ) + ) + } + + /** + * @important + * The resulting observable will complete as soon as the mutation + * is over, unlike the state which can be re-subscribed later. + */ + execute(variables: TVariables) { + this.executeSubject.next(variables) + this.executeSubject.complete() + + return this.observeTillFinished() + } + + continue() { + return this.execute(this.state.variables as TVariables) + } + + destroy() { + this.destroySubject.next() + this.destroySubject.complete() + this.executeSubject.complete() + } + + reset() { + this.resetSubject.next() + this.resetSubject.complete() + this.destroy() + } +} diff --git a/src/lib/queries/client/mutations/mutation/executeMutation.ts b/src/lib/queries/client/mutations/mutation/executeMutation.ts new file mode 100644 index 0000000..7c0afd8 --- /dev/null +++ b/src/lib/queries/client/mutations/mutation/executeMutation.ts @@ -0,0 +1,236 @@ +import { + map, + merge, + of, + switchMap, + concat, + toArray, + mergeMap, + takeWhile, + share, + iif, + catchError +} from "rxjs" +import { type MutationOptions, type MutationState } from "./types" +import { functionAsObservable } from "../../utils/functionAsObservable" +import { retryOnError } from "../../operators" +import { type DefaultError } from "../../types" +import { mergeResults } from "../operators" +import { type Mutation } from "./Mutation" +import { type MutationCache } from "../cache/MutationCache" + +export const executeMutation = < + TData = unknown, + TError = DefaultError, + TVariables = void, + TContext = unknown +>({ + variables, + state, + options, + mutation, + mutationCache +}: { + variables: TVariables + state: MutationState + options: MutationOptions + mutation: Mutation + mutationCache: MutationCache +}) => { + type LocalState = MutationState + + const isPaused = state.isPaused + + const defaultFn = async () => + await Promise.reject(new Error("No mutationFn found")) + + const mutationFn = options.mutationFn ?? defaultFn + + const onCacheMutate$ = iif( + () => isPaused, + of(null), + functionAsObservable( + () => + mutationCache.config.onMutate?.( + variables, + mutation as Mutation + ) + ) + ) + + const onOptionMutate$ = iif( + () => isPaused, + of(state.context), + functionAsObservable( + // eslint-disable-next-line @typescript-eslint/promise-function-async + () => options.onMutate?.(variables) ?? undefined + ) + ) + + const onMutate$ = onCacheMutate$.pipe( + mergeMap(() => onOptionMutate$), + share() + ) + + type QueryState = Omit, "data"> & { + // add layer to allow undefined as mutation result + result?: { data: TData } + } + + const onError = (error: TError, context: TContext, attempt: number) => { + console.error(error) + + const onCacheError$ = functionAsObservable( + () => + mutationCache.config.onError?.( + error as Error, + variables, + context, + mutation + ) + ) + + const onError$ = functionAsObservable( + () => options.onError?.(error, variables, context) + ) + + return concat(onCacheError$, onError$).pipe( + catchError(() => of(error)), + toArray(), + map( + (): QueryState => ({ + failureCount: attempt, + result: undefined, + error, + failureReason: error, + context, + status: "error" + }) + ) + ) + } + + const queryRunner$ = onMutate$.pipe( + switchMap((context) => { + const fn$ = + typeof mutationFn === "function" + ? // eslint-disable-next-line @typescript-eslint/promise-function-async + functionAsObservable(() => mutationFn(variables)) + : mutationFn + + return fn$.pipe( + map( + (data): QueryState => ({ + result: { + data + }, + error: null, + context + }) + ), + retryOnError({ + ...options, + caughtError: (attempt, error) => + of({ + failureCount: attempt, + failureReason: error + }), + catchError: (attempt, error) => + onError(error, context as TContext, attempt) + }), + takeWhile( + ({ result, error }) => + result?.data === undefined && error === undefined, + true + ) + ) + }) + ) + + const initState$ = of({ + ...state, + variables, + status: "pending", + isPaused: false, + failureCount: 0, + failureReason: null, + submittedAt: state.submittedAt ?? new Date().getTime() + } satisfies LocalState & Required>) + + const mutation$ = merge( + initState$, + onMutate$.pipe(map((context) => ({ context }))), + queryRunner$.pipe( + switchMap(({ result: mutationData, error, ...restState }) => { + if (!mutationData && !error) + return of({ + ...restState + }) + + const onSuccess$ = error + ? of(null) + : functionAsObservable( + () => + options.onSuccess?.( + mutationData?.data as TData, + variables, + restState.context + ) + ) + + // to pass as option from cache not here + const onCacheSettled$ = functionAsObservable( + () => + mutationCache.config.onSettled?.( + mutationData?.data, + error as any, + variables, + restState.context, + mutation as Mutation + ) + ) + + const onOptionSettled$ = functionAsObservable( + () => + options.onSettled?.( + mutationData?.data, + error as TError, + variables, + restState.context + ) + ) + + const onSettled$ = concat(onCacheSettled$, onOptionSettled$).pipe( + catchError((error) => (mutationData ? of(mutationData) : of(error))) + ) + + const result$ = concat(onSuccess$, onSettled$).pipe( + toArray(), + map(() => + error + ? ({ + error, + data: undefined, + variables, + ...restState + } satisfies Partial) + : ({ + status: "success" as const, + error, + data: mutationData?.data, + variables, + ...restState + } satisfies Partial) + ), + catchError((error) => + onError(error, restState.context as TContext, 0) + ) + ) + + return result$ + }) + ) + ).pipe(mergeResults) + + return mutation$ +} diff --git a/src/lib/queries/client/mutations/mutation/types.ts b/src/lib/queries/client/mutations/mutation/types.ts new file mode 100644 index 0000000..bab6e6f --- /dev/null +++ b/src/lib/queries/client/mutations/mutation/types.ts @@ -0,0 +1,90 @@ +import { type MonoTypeOperatorFunction } from "rxjs" +import { type DefaultError, type Query, type QueryResult, type Register } from "../../types" +import { type MapOperator, type MutationFn, type MutationKey } from "../types" + +export type MutationStatus = "idle" | "pending" | "success" | "error" + +export interface MutationState< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown +> { + context: TContext | undefined + data: TData | undefined + error: TError | null + status: MutationStatus + variables: TVariables | undefined + submittedAt: number + failureCount: number + failureReason: TError | null + isPaused: boolean +} + +export type MutationMeta = Register extends { + mutationMeta: infer TMutationMeta +} + ? TMutationMeta + : Record + +export interface MutationOptions< + TData, + TError = DefaultError, + TVariables = void, + TContext = unknown +> { + enabled?: boolean + retry?: false | number | ((attempt: number, error: unknown) => boolean) + retryDelay?: number | ((failureCount: number, error: TError) => number) + gcTime?: number + /** + * @important + * The hook with the lowest value will be taken into account + */ + staleTime?: number + /** + * Force the new query to be marked as stale. Only on first trigger + */ + markStale?: boolean + cacheTime?: number + /** + * @important + * interval is paused until the query finish fetching. This avoid infinite + * loop of refetch + */ + refetchInterval?: + | number + | false + | (( + data: QueryResult["data"] | undefined, + query: Query + ) => number | false) + terminateOnFirstResult?: boolean + onMutate?: ( + variables: TVariables + ) => Promise | TContext | undefined + onError?: ( + error: TError, + variables: TVariables, + context: TContext | undefined + ) => Promise | unknown + onSuccess?: ( + data: TData, + variables: TVariables, + context: TContext | undefined + ) => Promise | unknown + onSettled?: ( + data: TData | undefined, + error: TError | null, + variables: TVariables, + context: TContext | undefined + ) => Promise | unknown + mutationFn?: MutationFn + mutationKey?: MutationKey + mapOperator?: MapOperator + meta?: MutationMeta + __queryInitHook?: MonoTypeOperatorFunction + __queryRunnerHook?: MonoTypeOperatorFunction + __queryTriggerHook?: MonoTypeOperatorFunction> + __queryFinalizeHook?: MonoTypeOperatorFunction> +} diff --git a/src/lib/queries/client/mutations/mutations.test.ts b/src/lib/queries/client/mutations/mutations.test.ts index b4db98b..b100a83 100644 --- a/src/lib/queries/client/mutations/mutations.test.ts +++ b/src/lib/queries/client/mutations/mutations.test.ts @@ -3,8 +3,8 @@ import { type QueryClient } from "../createClient" import { createQueryClient, sleep } from "../../../../tests/utils" import { MutationObserver } from "./observers/MutationObserver" import { executeMutation, queryKey } from "../tests/utils" -import { type MutationState } from "./types" import { waitFor } from "@testing-library/react" +import { type MutationState } from "./mutation/types" describe("mutations", () => { let queryClient: QueryClient diff --git a/src/lib/queries/client/mutations/observers/MutationObserver.ts b/src/lib/queries/client/mutations/observers/MutationObserver.ts index 2056ae7..7a43cb9 100644 --- a/src/lib/queries/client/mutations/observers/MutationObserver.ts +++ b/src/lib/queries/client/mutations/observers/MutationObserver.ts @@ -10,15 +10,11 @@ import { last, tap } from "rxjs" -import { - type MutateOptions, - type MutationState, - type MutationFilters -} from "../types" +import { type MutateOptions, type MutationFilters } from "../types" import { getDefaultMutationState } from "../defaultMutationState" import { type QueryClient } from "../../createClient" import { type DefaultError } from "../../types" -import { type Mutation } from "../Mutation" +import { type Mutation } from "../mutation/Mutation" import { nanoid } from "../../keys/nanoid" import { type MutationObserverOptions, @@ -28,6 +24,7 @@ import { type MutationRunner, createMutationRunner } from "../runners/MutationRunner" +import { type MutationState } from "../mutation/types" /** * Provide API to observe mutations results globally. diff --git a/src/lib/queries/client/mutations/observers/types.ts b/src/lib/queries/client/mutations/observers/types.ts index 9e39980..cd253ed 100644 --- a/src/lib/queries/client/mutations/observers/types.ts +++ b/src/lib/queries/client/mutations/observers/types.ts @@ -1,10 +1,10 @@ import { type DefaultError } from "../../types" import { - type MutateFunction, type MutationOptions, type MutationState, type MutationStatus -} from "../types" +} from "../mutation/types" +import { type MutateFunction } from "../types" export interface MutationObserverOptions< TData = unknown, diff --git a/src/lib/queries/client/mutations/runners/MutationRunner.ts b/src/lib/queries/client/mutations/runners/MutationRunner.ts index cd37e50..5eacf76 100644 --- a/src/lib/queries/client/mutations/runners/MutationRunner.ts +++ b/src/lib/queries/client/mutations/runners/MutationRunner.ts @@ -1,11 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { BehaviorSubject, - Observable, - ObservedValueOf, + type ObservedValueOf, Subject, combineLatest, - concat, concatMap, defer, distinctUntilChanged, @@ -16,7 +14,6 @@ import { map, merge, mergeMap, - of, scan, shareReplay, skip, @@ -27,14 +24,14 @@ import { tap } from "rxjs" import { isDefined } from "../../../../utils/isDefined" -import { MutationState, type MutationOptions } from "../types" -import { mergeResults } from "../operators" import { type DefaultError } from "../../types" import { type MutationCache } from "../cache/MutationCache" import { type MutationObserverOptions } from "../observers/types" -import { Mutation } from "../Mutation" +import { type Mutation } from "../mutation/Mutation" import { shallowEqual } from "../../../../utils/shallowEqual" import { getDefaultMutationState } from "../defaultMutationState" +import { trackSubscriptions } from "../../../../utils/operators/trackSubscriptions" +import { type MutationOptions, type MutationState } from "../mutation/types" export type MutationRunner< TData, @@ -216,19 +213,9 @@ export const createMutationRunner = < ) }), shareReplay(1), - (source) => { - return new Observable>( - (observer) => { - refCountSubject.next(refCountSubject.getValue() + 1) - const sub = source.subscribe(observer) - - return () => { - refCountSubject.next(refCountSubject.getValue() - 1) - sub.unsubscribe() - } - } - ) - } + trackSubscriptions((count) => { + refCountSubject.next(count) + }) ) /** diff --git a/src/lib/queries/client/mutations/types.ts b/src/lib/queries/client/mutations/types.ts index a19c0e9..8114367 100644 --- a/src/lib/queries/client/mutations/types.ts +++ b/src/lib/queries/client/mutations/types.ts @@ -1,11 +1,9 @@ -import { type MonoTypeOperatorFunction, type Observable } from "rxjs" +import { type Observable } from "rxjs" import { - type Register, type DefaultError, - type Query, - type QueryResult } from "../types" -import { type Mutation } from "./Mutation" +import { type Mutation } from "./mutation/Mutation" +import { type MutationStatus } from "./mutation/types" /** * The default value `merge` is suitable for most use case. @@ -28,8 +26,6 @@ import { type Mutation } from "./Mutation" */ export type MapOperator = "switch" | "concat" | "merge" -export type MutationStatus = "idle" | "pending" | "success" | "error" - export type MutationKey = unknown[] /** @@ -66,91 +62,6 @@ export type MutationFn = | ((arg: MutationArg) => Promise) | ((arg: MutationArg) => Observable) -export type MutationMeta = Register extends { - mutationMeta: infer TMutationMeta -} - ? TMutationMeta - : Record - -export interface MutationOptions< - TData, - TError = DefaultError, - TVariables = void, - TContext = unknown -> { - enabled?: boolean - retry?: false | number | ((attempt: number, error: unknown) => boolean) - retryDelay?: number | ((failureCount: number, error: TError) => number) - gcTime?: number - /** - * @important - * The hook with the lowest value will be taken into account - */ - staleTime?: number - /** - * Force the new query to be marked as stale. Only on first trigger - */ - markStale?: boolean - cacheTime?: number - /** - * @important - * interval is paused until the query finish fetching. This avoid infinite - * loop of refetch - */ - refetchInterval?: - | number - | false - | (( - data: QueryResult["data"] | undefined, - query: Query - ) => number | false) - terminateOnFirstResult?: boolean - onMutate?: ( - variables: TVariables - ) => Promise | TContext | undefined - onError?: ( - error: TError, - variables: TVariables, - context: TContext | undefined - ) => Promise | unknown - onSuccess?: ( - data: TData, - variables: TVariables, - context: TContext | undefined - ) => Promise | unknown - onSettled?: ( - data: TData | undefined, - error: TError | null, - variables: TVariables, - context: TContext | undefined - ) => Promise | unknown - mutationFn?: MutationFn - mutationKey?: MutationKey - mapOperator?: MapOperator - meta?: MutationMeta - __queryInitHook?: MonoTypeOperatorFunction - __queryRunnerHook?: MonoTypeOperatorFunction - __queryTriggerHook?: MonoTypeOperatorFunction> - __queryFinalizeHook?: MonoTypeOperatorFunction> -} - -export interface MutationState< - TData = unknown, - TError = unknown, - TVariables = void, - TContext = unknown -> { - context: TContext | undefined - data: TData | undefined - error: TError | null - status: MutationStatus - variables: TVariables | undefined - submittedAt: number - failureCount: number - failureReason: TError | null - isPaused: boolean -} - export interface MutateOptions< TData = unknown, TError = DefaultError, diff --git a/src/lib/queries/client/operators.ts b/src/lib/queries/client/operators.ts index a77d0e7..e86979d 100644 --- a/src/lib/queries/client/operators.ts +++ b/src/lib/queries/client/operators.ts @@ -1,7 +1,7 @@ import { type Observable, distinctUntilChanged, scan } from "rxjs" import { shallowEqual } from "../../utils/shallowEqual" import { type QueryResult } from "./types" -import { type RetryBackoffConfig, retryBackoff } from "../../utils/retryBackoff" +import { type RetryBackoffConfig, retryBackoff } from "../../utils/operators/retryBackoff" export const retryOnError = ({ retryDelay, diff --git a/src/lib/queries/react/mutations/useMutation.rq.test.tsx b/src/lib/queries/react/mutations/useMutation.rq.test.tsx index 8bdc2d1..5ae0eca 100644 --- a/src/lib/queries/react/mutations/useMutation.rq.test.tsx +++ b/src/lib/queries/react/mutations/useMutation.rq.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/array-type */ /* eslint-disable @typescript-eslint/no-misused-promises */ @@ -18,7 +19,7 @@ import { import { useMutation } from "./useMutation" import { queryKey } from "../../client/tests/utils" import { type UseMutationResult } from "./types" -import { ErrorBoundary } from "react-error-boundary" +// import { ErrorBoundary } from "react-error-boundary" describe("useMutation", () => { // const queryCache = new QueryCache() @@ -1118,35 +1119,35 @@ describe("useMutation", () => { expect(onError).toHaveBeenCalledWith(mutateFnError, "todo", undefined) }) - it("should use provided custom queryClient", async () => { - function Page() { - const mutation = useMutation( - { - mutationFn: async (text: string) => { - return Promise.resolve(text) - } - }, - queryClient - ) + it("should use provided custom queryClient", async () => { + function Page() { + const mutation = useMutation( + { + mutationFn: async (text: string) => { + return Promise.resolve(text) + } + }, + queryClient + ) - return ( + return ( +
+
- -
- data: {mutation.data ?? "null"}, status: {mutation.status} -
+ data: {mutation.data ?? "null"}, status: {mutation.status}
- ) - } +
+ ) + } - const rendered = render() + const rendered = render() - await rendered.findByText("data: null, status: idle") + await rendered.findByText("data: null, status: idle") - fireEvent.click(rendered.getByRole("button", { name: /mutate/i })) + fireEvent.click(rendered.getByRole("button", { name: /mutate/i })) - await rendered.findByText("data: custom client, status: success") - }) + await rendered.findByText("data: custom client, status: success") + }) }) diff --git a/src/lib/queries/react/mutations/useMutationState.ts b/src/lib/queries/react/mutations/useMutationState.ts index 3e56616..2cf1d71 100644 --- a/src/lib/queries/react/mutations/useMutationState.ts +++ b/src/lib/queries/react/mutations/useMutationState.ts @@ -6,7 +6,7 @@ import { type MutationFilters } from "../../client/mutations/types" import { useLiveRef } from "../../../utils/useLiveRef" -import { type Mutation } from "../../client/mutations/Mutation" +import { type Mutation } from "../../client/mutations/mutation/Mutation" import { skip } from "rxjs" import { serializeKey } from "../../client/keys/serializeKey" import { createPredicateForFilters } from "../../client/mutations/filters" diff --git a/src/lib/utils/emitToSubject.ts b/src/lib/utils/operators/emitToSubject.ts similarity index 100% rename from src/lib/utils/emitToSubject.ts rename to src/lib/utils/operators/emitToSubject.ts diff --git a/src/lib/utils/retryBackoff.ts b/src/lib/utils/operators/retryBackoff.ts similarity index 97% rename from src/lib/utils/retryBackoff.ts rename to src/lib/utils/operators/retryBackoff.ts index 8879557..ccb17f1 100644 --- a/src/lib/utils/retryBackoff.ts +++ b/src/lib/utils/operators/retryBackoff.ts @@ -38,7 +38,7 @@ export function exponentialBackoffDelay( * of an error. If the source Observable calls error, rather than propagating * the error call this method will resubscribe to the source Observable with * exponentially increasing interval and up to a maximum of count - * resubscriptions (if provided). Retrying can be cancelled at any point if + * re-subscriptions (if provided). Retrying can be cancelled at any point if * shouldRetry returns false. */ export function retryBackoff(config: RetryBackoffConfig) { diff --git a/src/lib/utils/shareLatest.ts b/src/lib/utils/operators/shareLatest.ts similarity index 100% rename from src/lib/utils/shareLatest.ts rename to src/lib/utils/operators/shareLatest.ts diff --git a/src/lib/utils/operators/trackSubscriptions.ts b/src/lib/utils/operators/trackSubscriptions.ts new file mode 100644 index 0000000..48e7bba --- /dev/null +++ b/src/lib/utils/operators/trackSubscriptions.ts @@ -0,0 +1,21 @@ +import { Observable } from "rxjs" + +export function trackSubscriptions( + onCountUpdate: (activeSubscriptions: number) => void +) { + let count = 0 + + return function refCountOperatorFunction(source: Observable) { + return new Observable((observer) => { + count++ + onCountUpdate(count) + const sub = source.subscribe(observer) + + return () => { + count-- + onCountUpdate(count) + sub.unsubscribe() + } + }) + } +} diff --git a/src/lib/utils/trackSubscriptions.ts b/src/lib/utils/trackSubscriptions.ts deleted file mode 100644 index 32c8c69..0000000 --- a/src/lib/utils/trackSubscriptions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type Observable, defer, finalize } from "rxjs" - -export function trackSubscriptions( - onCountUpdate: (activeSubscriptions: number) => void -) { - return function refCountOperatorFunction(source$: Observable) { - let counter = 0 - - return defer(() => { - counter++ - onCountUpdate(counter) - return source$ - }).pipe( - finalize(() => { - counter-- - onCountUpdate(counter) - }) - ) - } -}