import { focusManager } from './focusManager' import { MutationCache, MutationInfoFilters, createMutationCache, } from './mutationCache' import { MutationInfoOptions } from './mutationInfo' import { ObservableInfiniteQuery, ObservableInfiniteQueryOptions, createObservableInfiniteQuery, } from './observableInfiniteQuery' import { ObservableQuery, ObservableQueryOptions, createObservableQuery, } from './observableQuery' import { onlineManager } from './onlineManager' import { PrimitiveQuery, isInfiniteQuery } from './primitiveQuery' import { QueryCache, QueryInfoFilters, QueryInfoTypeFilter, createQueryCache, } from './queryCache' import { QueryInfo, QueryInfoOptions, QueryInfoState, SetDataOptions, } from './queryInfo' import { DeepPartial, GetVariablesOption, Updater } from './typeUtils' import { InfiniteData } from './types' import { UNDEFINED, functionalUpdate, getFullKey, hashKeyByOptions, isUndefined, noop, } from './utils' export interface ResultOptions { throwOnError?: boolean } export interface RefetchOptions extends ResultOptions { cancelRefetch?: boolean } export interface InvalidateOptions extends RefetchOptions {} export interface ResetOptions extends RefetchOptions {} export interface CancelOptions { revert?: boolean silent?: boolean } export interface InvalidateQueryFilters< TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData > extends QueryInfoFilters { refetchType?: QueryInfoTypeFilter | 'none' } export type FetchQueryOptions< TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData > = Omit< QueryInfoOptions, 'variables' | '_defaulted' > & GetVariablesOption & { /** * The time in milliseconds after data is considered stale. * If the data is fresh it will be returned from the cache. */ staleTime?: number } export type TriggerMutationOptions< TData = unknown, TVars = unknown, TError = Error > = Omit, '_defaulted'> & GetVariablesOption export interface DefaultOptions { queries?: Omit, 'query' | '_defaulted'> mutations?: Omit, 'mutation' | '_defaulted'> } export interface QueryClientConfig { defaultOptions?: DefaultOptions queryCache?: QueryCache mutationCache?: MutationCache } export interface QueryClient extends ReturnType {} export const createQueryClient = (config: QueryClientConfig = {}) => { let mountCount = 0 let unsubscribeFocus: (() => void) | undefined let unsubscribeOnline: (() => void) | undefined let defaultOptions = config.defaultOptions const mount = (): void => { mountCount++ if (mountCount !== 1) return unsubscribeFocus = focusManager.subscribe(() => { if (focusManager.isFocused()) { queryCache.onFocus() } }) unsubscribeOnline = onlineManager.subscribe(() => { if (onlineManager.isOnline()) { queryCache.onOnline() } }) } const unmount = (): void => { mountCount-- if (mountCount !== 0) return unsubscribeFocus?.() unsubscribeFocus = UNDEFINED unsubscribeOnline?.() unsubscribeOnline = UNDEFINED } const queryCache = config.queryCache ?? createQueryCache() const mutationCache = config.mutationCache ?? createMutationCache() const getQueryCache = () => queryCache const getMutationCache = () => mutationCache const getDefaultOptions = () => defaultOptions const setDefaultOptions = (options: DefaultOptions): void => { defaultOptions = options } const fetchQuery = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( options: FetchQueryOptions ): Promise => { const defaultedOptions = defaultQueryOptions(options) if (isUndefined(defaultedOptions.retry)) { defaultedOptions.retry = false } const queryInfo = queryCache.build(client, defaultedOptions) return queryInfo.isStaleByTime(defaultedOptions.staleTime) ? queryInfo.fetch(defaultedOptions) : Promise.resolve(queryInfo.state.data!) } const prefetchQuery = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( options: FetchQueryOptions ) => { return fetchQuery(options).then(noop).catch(noop) } const refetchQueries = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters?: QueryInfoFilters, options?: RefetchOptions ): Promise => { const fetchOptions = { ...options, cancelRefetch: options?.cancelRefetch ?? true, } const promises = queryCache .findAll(filters) .filter(queryInfo => !queryInfo.isDisabled()) .map(queryInfo => { let promise = queryInfo.fetch(UNDEFINED, fetchOptions) if (!fetchOptions.throwOnError) { promise = promise.catch(noop) as Promise } return queryInfo.state.fetchStatus === 'paused' ? Promise.resolve() : promise }) return Promise.all(promises).then(noop) } const getQueryState = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters: { query: PrimitiveQuery } & GetVariablesOption ): QueryInfoState | undefined => { return queryCache.find( filters as { query: PrimitiveQuery variables: DeepPartial } )?.state } const getQueryData = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters: { query: PrimitiveQuery } & GetVariablesOption ): TQueryData | undefined => { return getQueryState(filters)?.data } const setQueryData = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters: { query: PrimitiveQuery } & GetVariablesOption, updater: Updater, setDataOptions?: SetDataOptions ) => { const defaultedOptions = defaultQueryOptions(filters) const queryInfo = queryCache.get( defaultedOptions.queryHash! ) const prevData = queryInfo?.state.data const data = functionalUpdate(updater, prevData) return isUndefined(data) ? UNDEFINED : queryCache.build(client, defaultedOptions).setData(data, setDataOptions) } const getQueriesData = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters?: QueryInfoFilters ): [ QueryInfo, TQueryData | undefined ][] => { return queryCache .findAll(filters) .map(queryInfo => [queryInfo, queryInfo.state.data]) } const setQueriesData = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters: QueryInfoFilters, updater: Updater, options?: SetDataOptions ) => { return queryCache .findAll(filters) .map(queryInfo => [ queryInfo, setQueryData(queryInfo as any, updater, options), ]) } const removeQueries = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters?: QueryInfoFilters ) => { queryCache .findAll(filters) .forEach(queryInfo => queryCache.remove(queryInfo)) } const resetQueries = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters?: QueryInfoFilters, options?: ResetOptions ): Promise => { const refetchFilters: QueryInfoFilters< TFetcherData, TVars, TError, TQueryData > = { type: 'active', ...filters, } queryCache.findAll(filters).forEach(queryInfo => queryInfo.reset()) return refetchQueries(refetchFilters, options) } const invalidateQueries = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters: InvalidateQueryFilters< TFetcherData, TVars, TError, TQueryData > = {}, options: InvalidateOptions = {} ): Promise => { queryCache.findAll(filters).forEach(q => q.invalidate()) if (filters.refetchType === 'none') { return Promise.resolve() } const refetchFilters: QueryInfoFilters< TFetcherData, TVars, TError, TQueryData > = { ...filters, type: filters.refetchType ?? filters.type ?? 'active', } return refetchQueries(refetchFilters, options) } const cancelQueries = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters?: QueryInfoFilters, cancelOptions: CancelOptions = {} ): Promise => { const defaultedCancelOptions = { revert: true, ...cancelOptions } const promises = queryCache .findAll(filters) .map(queryInfo => queryInfo.cancel(defaultedCancelOptions)) return Promise.all(promises).then(noop).catch(noop) } const defaultQueryOptions = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData, TData = TQueryData >( options?: ObservableQueryOptions< TFetcherData, TVars, TError, TQueryData, TData > ): ObservableQueryOptions => { if (options?._defaulted) { return options as ObservableQueryOptions< TFetcherData, TVars, TError, TQueryData, TData > } const defaultedOptions = { ...defaultOptions?.queries, ...options?.query, ...options, _defaulted: true, } as ObservableQueryOptions // dependent default values if (isUndefined(defaultedOptions.refetchOnReconnect)) { defaultedOptions.refetchOnReconnect = defaultedOptions.networkMode !== 'always' } if (isUndefined(defaultedOptions.throwOnError)) { defaultedOptions.throwOnError = !!defaultedOptions.suspense } if (defaultedOptions.query && !defaultedOptions.queryHash) { defaultedOptions.queryHash = hashKeyByOptions( getFullKey(defaultedOptions.query.key, defaultedOptions.variables), defaultedOptions ) } return defaultedOptions } const defaultMutationOptions = >( options?: T ): T => { if (options?._defaulted) { return options } return { ...defaultOptions?.mutations, ...options?.mutation, ...options, _defaulted: true, } as T } const ensureQueryData = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( options: FetchQueryOptions ): Promise => { const cachedData = getQueryData(options) return cachedData ? Promise.resolve(cachedData) : fetchQuery(options) } const triggerMutation = ( options: TriggerMutationOptions ) => { return mutationCache.build(client, options).trigger(options.variables!) } const isMutating = ( filters?: MutationInfoFilters ): number => { return mutationCache.findAll({ ...filters, status: 'mutating' }).length } const isFetching = (filters?: QueryInfoFilters): number => { return queryCache.findAll({ ...filters, fetchStatus: 'fetching' }).length } const clear = (): void => { queryCache.clear() mutationCache.clear() } function watchQuery< TFetcherData = unknown, TVars = unknown, TError = Error, TData = InfiniteData >( options: ObservableInfiniteQueryOptions ): ObservableInfiniteQuery function watchQuery< TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData, TData = TQueryData >( options: ObservableQueryOptions< TFetcherData, TVars, TError, TQueryData, TData > ): ObservableQuery function watchQuery(options: any): any { return isInfiniteQuery(options.query) ? createObservableInfiniteQuery(client, options) : createObservableQuery(client, options) } const client = { getMutationCache, defaultQueryOptions, defaultMutationOptions, getDefaultOptions, setDefaultOptions, getQueryCache, fetchQuery, prefetchQuery, refetchQueries, invalidateQueries, resetQueries, cancelQueries, setQueryData, setQueriesData, removeQueries, getQueryState, getQueryData, getQueriesData, ensureQueryData, triggerMutation, isMutating, isFetching, watchQuery, mount, unmount, clear, } return client }