import { functionalUpdate, hashKey, hashQueryKeyByOptions, noop, partialMatchKey, resolveStaleTime, skipToken, } from './utils' import { QueryCache } from './queryCache' import { MutationCache } from './mutationCache' import { focusManager } from './focusManager' import { onlineManager } from './onlineManager' import { notifyManager } from './notifyManager' import { infiniteQueryBehavior } from './infiniteQueryBehavior' import type { CancelOptions, DefaultError, DefaultOptions, DefaultedQueryObserverOptions, EnsureInfiniteQueryDataOptions, EnsureQueryDataOptions, FetchInfiniteQueryOptions, FetchQueryOptions, InferDataFromTag, InferErrorFromTag, InfiniteData, InvalidateOptions, InvalidateQueryFilters, MutationKey, MutationObserverOptions, MutationOptions, NoInfer, OmitKeyof, QueryClientConfig, QueryKey, QueryObserverOptions, QueryOptions, RefetchOptions, RefetchQueryFilters, ResetOptions, SetDataOptions, } from './types' import type { QueryState } from './query' import type { MutationFilters, QueryFilters, Updater } from './utils' // TYPES interface QueryDefaults { queryKey: QueryKey defaultOptions: OmitKeyof, 'queryKey'> } interface MutationDefaults { mutationKey: MutationKey defaultOptions: MutationOptions } // CLASS export class QueryClient { #queryCache: QueryCache #mutationCache: MutationCache #defaultOptions: DefaultOptions #queryDefaults: Map #mutationDefaults: Map #mountCount: number #unsubscribeFocus?: () => void #unsubscribeOnline?: () => void constructor(config: QueryClientConfig = {}) { this.#queryCache = config.queryCache || new QueryCache() this.#mutationCache = config.mutationCache || new MutationCache() this.#defaultOptions = config.defaultOptions || {} this.#queryDefaults = new Map() this.#mutationDefaults = new Map() this.#mountCount = 0 } mount(): void { this.#mountCount++ if (this.#mountCount !== 1) return this.#unsubscribeFocus = focusManager.subscribe(async (focused) => { if (focused) { await this.resumePausedMutations() this.#queryCache.onFocus() } }) this.#unsubscribeOnline = onlineManager.subscribe(async (online) => { if (online) { await this.resumePausedMutations() this.#queryCache.onOnline() } }) } unmount(): void { this.#mountCount-- if (this.#mountCount !== 0) return this.#unsubscribeFocus?.() this.#unsubscribeFocus = undefined this.#unsubscribeOnline?.() this.#unsubscribeOnline = undefined } isFetching = QueryFilters>( filters?: TQueryFilters, ): number { return this.#queryCache.findAll({ ...filters, fetchStatus: 'fetching' }) .length } isMutating< TMutationFilters extends MutationFilters = MutationFilters, >(filters?: TMutationFilters): number { return this.#mutationCache.findAll({ ...filters, status: 'pending' }).length } /** * Imperative (non-reactive) way to retrieve data for a QueryKey. * Should only be used in callbacks or functions where reading the latest data is necessary, e.g. for optimistic updates. * * Hint: Do not use this function inside a component, because it won't receive updates. * Use `useQuery` to create a `QueryObserver` that subscribes to changes. */ getQueryData< TQueryFnData = unknown, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, >(queryKey: TTaggedQueryKey): TInferredQueryFnData | undefined { const options = this.defaultQueryOptions({ queryKey }) return this.#queryCache.get(options.queryHash)?.state .data } ensureQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: EnsureQueryDataOptions, ): Promise { const defaultedOptions = this.defaultQueryOptions(options) const query = this.#queryCache.build(this, defaultedOptions) const cachedData = query.state.data if (cachedData === undefined) { return this.fetchQuery(options) } if ( options.revalidateIfStale && query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query)) ) { void this.prefetchQuery(defaultedOptions) } return Promise.resolve(cachedData) } getQueriesData< TQueryFnData = unknown, TQueryFilters extends QueryFilters = QueryFilters, >(filters: TQueryFilters): Array<[QueryKey, TQueryFnData | undefined]> { return this.#queryCache.findAll(filters).map(({ queryKey, state }) => { const data = state.data as TQueryFnData | undefined return [queryKey, data] }) } setQueryData< TQueryFnData = unknown, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, >( queryKey: TTaggedQueryKey, updater: Updater< NoInfer | undefined, NoInfer | undefined >, options?: SetDataOptions, ): NoInfer | undefined { const defaultedOptions = this.defaultQueryOptions< any, any, unknown, any, QueryKey >({ queryKey }) const query = this.#queryCache.get( defaultedOptions.queryHash, ) const prevData = query?.state.data const data = functionalUpdate(updater, prevData) if (data === undefined) { return undefined } return this.#queryCache .build(this, defaultedOptions) .setData(data, { ...options, manual: true }) } setQueriesData< TQueryFnData, TQueryFilters extends QueryFilters = QueryFilters, >( filters: TQueryFilters, updater: Updater< NoInfer | undefined, NoInfer | undefined >, options?: SetDataOptions, ): Array<[QueryKey, TQueryFnData | undefined]> { return notifyManager.batch(() => this.#queryCache .findAll(filters) .map(({ queryKey }) => [ queryKey, this.setQueryData(queryKey, updater, options), ]), ) } getQueryState< TQueryFnData = unknown, TError = DefaultError, TTaggedQueryKey extends QueryKey = QueryKey, TInferredQueryFnData = InferDataFromTag, TInferredError = InferErrorFromTag, >( queryKey: TTaggedQueryKey, ): QueryState | undefined { const options = this.defaultQueryOptions({ queryKey }) return this.#queryCache.get( options.queryHash, )?.state } removeQueries( filters?: QueryFilters, ): void { const queryCache = this.#queryCache notifyManager.batch(() => { queryCache.findAll(filters).forEach((query) => { queryCache.remove(query) }) }) } resetQueries( filters?: QueryFilters, options?: ResetOptions, ): Promise { const queryCache = this.#queryCache return notifyManager.batch(() => { queryCache.findAll(filters).forEach((query) => { query.reset() }) return this.refetchQueries( { type: 'active', ...filters, }, options, ) }) } cancelQueries( filters?: QueryFilters, cancelOptions: CancelOptions = {}, ): Promise { const defaultedCancelOptions = { revert: true, ...cancelOptions } const promises = notifyManager.batch(() => this.#queryCache .findAll(filters) .map((query) => query.cancel(defaultedCancelOptions)), ) return Promise.all(promises).then(noop).catch(noop) } invalidateQueries( filters?: InvalidateQueryFilters, options: InvalidateOptions = {}, ): Promise { return notifyManager.batch(() => { this.#queryCache.findAll(filters).forEach((query) => { query.invalidate() }) if (filters?.refetchType === 'none') { return Promise.resolve() } return this.refetchQueries( { ...filters, type: filters?.refetchType ?? filters?.type ?? 'active', }, options, ) }) } refetchQueries( filters?: RefetchQueryFilters, options: RefetchOptions = {}, ): Promise { const fetchOptions = { ...options, cancelRefetch: options.cancelRefetch ?? true, } const promises = notifyManager.batch(() => this.#queryCache .findAll(filters) .filter((query) => !query.isDisabled() && !query.isStatic()) .map((query) => { let promise = query.fetch(undefined, fetchOptions) if (!fetchOptions.throwOnError) { promise = promise.catch(noop) } return query.state.fetchStatus === 'paused' ? Promise.resolve() : promise }), ) return Promise.all(promises).then(noop) } fetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: FetchQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise { const defaultedOptions = this.defaultQueryOptions(options) // https://github.com/tannerlinsley/react-query/issues/652 if (defaultedOptions.retry === undefined) { defaultedOptions.retry = false } const query = this.#queryCache.build(this, defaultedOptions) return query.isStaleByTime( resolveStaleTime(defaultedOptions.staleTime, query), ) ? query.fetch(defaultedOptions) : Promise.resolve(query.state.data as TData) } prefetchQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: FetchQueryOptions, ): Promise { return this.fetchQuery(options).then(noop).catch(noop) } fetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise> { options.behavior = infiniteQueryBehavior< TQueryFnData, TError, TData, TPageParam >(options.pages) return this.fetchQuery(options as any) } prefetchInfiniteQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: FetchInfiniteQueryOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise { return this.fetchInfiniteQuery(options).then(noop).catch(noop) } ensureInfiniteQueryData< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, >( options: EnsureInfiniteQueryDataOptions< TQueryFnData, TError, TData, TQueryKey, TPageParam >, ): Promise> { options.behavior = infiniteQueryBehavior< TQueryFnData, TError, TData, TPageParam >(options.pages) return this.ensureQueryData(options as any) } resumePausedMutations(): Promise { if (onlineManager.isOnline()) { return this.#mutationCache.resumePausedMutations() } return Promise.resolve() } getQueryCache(): QueryCache { return this.#queryCache } getMutationCache(): MutationCache { return this.#mutationCache } getDefaultOptions(): DefaultOptions { return this.#defaultOptions } setDefaultOptions(options: DefaultOptions): void { this.#defaultOptions = options } setQueryDefaults< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, >( queryKey: QueryKey, options: Partial< OmitKeyof< QueryObserverOptions, 'queryKey' > >, ): void { this.#queryDefaults.set(hashKey(queryKey), { queryKey, defaultOptions: options, }) } getQueryDefaults( queryKey: QueryKey, ): OmitKeyof, 'queryKey'> { const defaults = [...this.#queryDefaults.values()] const result: OmitKeyof< QueryObserverOptions, 'queryKey' > = {} defaults.forEach((queryDefault) => { if (partialMatchKey(queryKey, queryDefault.queryKey)) { Object.assign(result, queryDefault.defaultOptions) } }) return result } setMutationDefaults< TData = unknown, TError = DefaultError, TVariables = void, TContext = unknown, >( mutationKey: MutationKey, options: OmitKeyof< MutationObserverOptions, 'mutationKey' >, ): void { this.#mutationDefaults.set(hashKey(mutationKey), { mutationKey, defaultOptions: options, }) } getMutationDefaults( mutationKey: MutationKey, ): OmitKeyof, 'mutationKey'> { const defaults = [...this.#mutationDefaults.values()] const result: OmitKeyof< MutationObserverOptions, 'mutationKey' > = {} defaults.forEach((queryDefault) => { if (partialMatchKey(mutationKey, queryDefault.mutationKey)) { Object.assign(result, queryDefault.defaultOptions) } }) return result } defaultQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >( options: | QueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey, TPageParam > | DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey >, ): DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > { if (options._defaulted) { return options as DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > } const defaultedOptions = { ...this.#defaultOptions.queries, ...this.getQueryDefaults(options.queryKey), ...options, _defaulted: true, } if (!defaultedOptions.queryHash) { defaultedOptions.queryHash = hashQueryKeyByOptions( defaultedOptions.queryKey, defaultedOptions, ) } // dependent default values if (defaultedOptions.refetchOnReconnect === undefined) { defaultedOptions.refetchOnReconnect = defaultedOptions.networkMode !== 'always' } if (defaultedOptions.throwOnError === undefined) { defaultedOptions.throwOnError = !!defaultedOptions.suspense } if (!defaultedOptions.networkMode && defaultedOptions.persister) { defaultedOptions.networkMode = 'offlineFirst' } if (defaultedOptions.queryFn === skipToken) { defaultedOptions.enabled = false } return defaultedOptions as DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey > } defaultMutationOptions>( options?: T, ): T { if (options?._defaulted) { return options } return { ...this.#defaultOptions.mutations, ...(options?.mutationKey && this.getMutationDefaults(options.mutationKey)), ...options, _defaulted: true, } as T } clear(): void { this.#queryCache.clear() this.#mutationCache.clear() } }