633 lines
16 KiB
TypeScript
633 lines
16 KiB
TypeScript
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,
|
|
DataTag,
|
|
DefaultError,
|
|
DefaultOptions,
|
|
DefaultedQueryObserverOptions,
|
|
EnsureInfiniteQueryDataOptions,
|
|
EnsureQueryDataOptions,
|
|
FetchInfiniteQueryOptions,
|
|
FetchQueryOptions,
|
|
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<QueryOptions<any, any, any>, 'queryKey'>
|
|
}
|
|
|
|
interface MutationDefaults {
|
|
mutationKey: MutationKey
|
|
defaultOptions: MutationOptions<any, any, any, any>
|
|
}
|
|
|
|
// CLASS
|
|
|
|
export class QueryClient {
|
|
#queryCache: QueryCache
|
|
#mutationCache: MutationCache
|
|
#defaultOptions: DefaultOptions
|
|
#queryDefaults: Map<string, QueryDefaults>
|
|
#mutationDefaults: Map<string, MutationDefaults>
|
|
#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(filters?: QueryFilters): number {
|
|
return this.#queryCache.findAll({ ...filters, fetchStatus: 'fetching' })
|
|
.length
|
|
}
|
|
|
|
isMutating(filters?: MutationFilters): number {
|
|
return this.#mutationCache.findAll({ ...filters, status: 'pending' }).length
|
|
}
|
|
|
|
getQueryData<
|
|
TQueryFnData = unknown,
|
|
TTaggedQueryKey extends QueryKey = QueryKey,
|
|
TInferredQueryFnData = TTaggedQueryKey extends DataTag<
|
|
unknown,
|
|
infer TaggedValue
|
|
>
|
|
? TaggedValue
|
|
: TQueryFnData,
|
|
>(queryKey: TTaggedQueryKey): TInferredQueryFnData | undefined
|
|
getQueryData(queryKey: QueryKey) {
|
|
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<TQueryFnData, TError, TData, TQueryKey>,
|
|
): Promise<TData> {
|
|
const cachedData = this.getQueryData<TData>(options.queryKey)
|
|
|
|
if (cachedData === undefined) return this.fetchQuery(options)
|
|
else {
|
|
const defaultedOptions = this.defaultQueryOptions(options)
|
|
const query = this.#queryCache.build(this, defaultedOptions)
|
|
|
|
if (
|
|
options.revalidateIfStale &&
|
|
query.isStaleByTime(resolveStaleTime(defaultedOptions.staleTime, query))
|
|
) {
|
|
void this.prefetchQuery(defaultedOptions)
|
|
}
|
|
|
|
return Promise.resolve(cachedData)
|
|
}
|
|
}
|
|
|
|
getQueriesData<TQueryFnData = unknown>(
|
|
filters: QueryFilters,
|
|
): 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 = TTaggedQueryKey extends DataTag<
|
|
unknown,
|
|
infer TaggedValue
|
|
>
|
|
? TaggedValue
|
|
: TQueryFnData,
|
|
>(
|
|
queryKey: TTaggedQueryKey,
|
|
updater: Updater<
|
|
NoInfer<TInferredQueryFnData> | undefined,
|
|
NoInfer<TInferredQueryFnData> | undefined
|
|
>,
|
|
options?: SetDataOptions,
|
|
): TInferredQueryFnData | undefined {
|
|
const defaultedOptions = this.defaultQueryOptions<
|
|
any,
|
|
any,
|
|
unknown,
|
|
any,
|
|
QueryKey
|
|
>({ queryKey })
|
|
|
|
const query = this.#queryCache.get<TInferredQueryFnData>(
|
|
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>(
|
|
filters: QueryFilters,
|
|
updater: Updater<TQueryFnData | undefined, TQueryFnData | undefined>,
|
|
options?: SetDataOptions,
|
|
): Array<[QueryKey, TQueryFnData | undefined]> {
|
|
return notifyManager.batch(() =>
|
|
this.#queryCache
|
|
.findAll(filters)
|
|
.map(({ queryKey }) => [
|
|
queryKey,
|
|
this.setQueryData<TQueryFnData>(queryKey, updater, options),
|
|
]),
|
|
)
|
|
}
|
|
|
|
getQueryState<
|
|
TQueryFnData = unknown,
|
|
TError = DefaultError,
|
|
TTaggedQueryKey extends QueryKey = QueryKey,
|
|
TInferredQueryFnData = TTaggedQueryKey extends DataTag<
|
|
unknown,
|
|
infer TaggedValue
|
|
>
|
|
? TaggedValue
|
|
: TQueryFnData,
|
|
>(
|
|
queryKey: TTaggedQueryKey,
|
|
): QueryState<TInferredQueryFnData, TError> | undefined {
|
|
const options = this.defaultQueryOptions({ queryKey })
|
|
return this.#queryCache.get<TInferredQueryFnData, TError>(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<void> {
|
|
const queryCache = this.#queryCache
|
|
|
|
const refetchFilters: RefetchQueryFilters = {
|
|
type: 'active',
|
|
...filters,
|
|
}
|
|
|
|
return notifyManager.batch(() => {
|
|
queryCache.findAll(filters).forEach((query) => {
|
|
query.reset()
|
|
})
|
|
return this.refetchQueries(refetchFilters, options)
|
|
})
|
|
}
|
|
|
|
cancelQueries(
|
|
filters: QueryFilters = {},
|
|
cancelOptions: CancelOptions = {},
|
|
): Promise<void> {
|
|
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<void> {
|
|
return notifyManager.batch(() => {
|
|
this.#queryCache.findAll(filters).forEach((query) => {
|
|
query.invalidate()
|
|
})
|
|
|
|
if (filters.refetchType === 'none') {
|
|
return Promise.resolve()
|
|
}
|
|
const refetchFilters: RefetchQueryFilters = {
|
|
...filters,
|
|
type: filters.refetchType ?? filters.type ?? 'active',
|
|
}
|
|
return this.refetchQueries(refetchFilters, options)
|
|
})
|
|
}
|
|
|
|
refetchQueries(
|
|
filters: RefetchQueryFilters = {},
|
|
options?: RefetchOptions,
|
|
): Promise<void> {
|
|
const fetchOptions = {
|
|
...options,
|
|
cancelRefetch: options?.cancelRefetch ?? true,
|
|
}
|
|
const promises = notifyManager.batch(() =>
|
|
this.#queryCache
|
|
.findAll(filters)
|
|
.filter((query) => !query.isDisabled())
|
|
.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<TData> {
|
|
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<TQueryFnData, TError, TData, TQueryKey>,
|
|
): Promise<void> {
|
|
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<InfiniteData<TData, TPageParam>> {
|
|
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<void> {
|
|
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<InfiniteData<TData, TPageParam>> {
|
|
options.behavior = infiniteQueryBehavior<
|
|
TQueryFnData,
|
|
TError,
|
|
TData,
|
|
TPageParam
|
|
>(options.pages)
|
|
|
|
return this.ensureQueryData(options as any)
|
|
}
|
|
|
|
resumePausedMutations(): Promise<unknown> {
|
|
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<TQueryFnData, TError, TData, TQueryData>,
|
|
'queryKey'
|
|
>
|
|
>,
|
|
): void {
|
|
this.#queryDefaults.set(hashKey(queryKey), {
|
|
queryKey,
|
|
defaultOptions: options,
|
|
})
|
|
}
|
|
|
|
getQueryDefaults(
|
|
queryKey: QueryKey,
|
|
): OmitKeyof<QueryObserverOptions<any, any, any, any, any>, 'queryKey'> {
|
|
const defaults = [...this.#queryDefaults.values()]
|
|
|
|
let result: OmitKeyof<
|
|
QueryObserverOptions<any, any, any, any, any>,
|
|
'queryKey'
|
|
> = {}
|
|
|
|
defaults.forEach((queryDefault) => {
|
|
if (partialMatchKey(queryKey, queryDefault.queryKey)) {
|
|
result = { ...result, ...queryDefault.defaultOptions }
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
setMutationDefaults<
|
|
TData = unknown,
|
|
TError = DefaultError,
|
|
TVariables = void,
|
|
TContext = unknown,
|
|
>(
|
|
mutationKey: MutationKey,
|
|
options: OmitKeyof<
|
|
MutationObserverOptions<TData, TError, TVariables, TContext>,
|
|
'mutationKey'
|
|
>,
|
|
): void {
|
|
this.#mutationDefaults.set(hashKey(mutationKey), {
|
|
mutationKey,
|
|
defaultOptions: options,
|
|
})
|
|
}
|
|
|
|
getMutationDefaults(
|
|
mutationKey: MutationKey,
|
|
): MutationObserverOptions<any, any, any, any> {
|
|
const defaults = [...this.#mutationDefaults.values()]
|
|
|
|
let result: MutationObserverOptions<any, any, any, any> = {}
|
|
|
|
defaults.forEach((queryDefault) => {
|
|
if (partialMatchKey(mutationKey, queryDefault.mutationKey)) {
|
|
result = { ...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.enabled !== true &&
|
|
defaultedOptions.queryFn === skipToken
|
|
) {
|
|
defaultedOptions.enabled = false
|
|
}
|
|
|
|
return defaultedOptions as DefaultedQueryObserverOptions<
|
|
TQueryFnData,
|
|
TError,
|
|
TData,
|
|
TQueryData,
|
|
TQueryKey
|
|
>
|
|
}
|
|
|
|
defaultMutationOptions<T extends MutationOptions<any, any, any, any>>(
|
|
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()
|
|
}
|
|
}
|