import { PrimitiveQuery } from './primitiveQuery' import { QueryClient } from './queryClient' import { Action, FetchStatus, QueryInfo, QueryInfoOptions, QueryInfoState, createQueryInfo, } from './queryInfo' import type { DeepPartial, NotifyEvent, WithRequired } from './typeUtils' import { UNDEFINED, getFullKey, hashKeyByOptions, isBoolean, isUndefined, partialMatchKey, } from './utils' export interface QueryCache extends ReturnType {} interface NotifyEventQueryAdded extends NotifyEvent { type: 'added' queryInfo: QueryInfo } interface NotifyEventQueryRemoved extends NotifyEvent { type: 'removed' queryInfo: QueryInfo } export interface NotifyEventQueryUpdated< TFetcherData, TVars, TError, TQueryData > extends NotifyEvent { type: 'updated' queryInfo: QueryInfo action: Action } export type QueryCacheNotifyEvent = | NotifyEventQueryAdded | NotifyEventQueryRemoved | NotifyEventQueryUpdated export interface QueryStore { has: (queryKey: string) => boolean set: (queryKey: string, queryInfo: QueryInfo) => void get: (queryKey: string) => QueryInfo | undefined delete: (queryKey: string) => void values: () => IterableIterator> } type QueryCacheListeners = ( event: QueryCacheNotifyEvent ) => void export const createQueryCache = ( config: { onError?: (error: Error, queryInfo: QueryInfo) => void onSuccess?: (data: unknown, query: QueryInfo) => void onSettled?: (data: unknown, error: Error, query: QueryInfo) => void createStore?: () => QueryStore } = {} ) => { let lastUpdated = 0 const queries: QueryStore = config?.createStore?.() ?? new Map() const listeners = new Set>() function subscribe< TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters: QueryInfoFilters, listener: QueryCacheListeners ): () => void function subscribe< TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( listener: QueryCacheListeners ): () => void function subscribe(...args: any) { const [filters, listener] = args.length === 2 ? args : [UNDEFINED, args[0]] const wrappedListener = ( event: QueryCacheNotifyEvent ) => { if (!filters || matchQueryInfo(filters, event.queryInfo)) { listener(event) } } listeners.add(wrappedListener) return () => { listeners.delete(wrappedListener) } } const getAll = (): QueryInfo[] => { return Array.from(queries.values()) } const find = ( filters: WithRequired< QueryInfoFilters, 'query' > ): QueryInfo | undefined => { const defaultedFilters = { exact: true, ...filters } return getAll().find(queryInfo => matchQueryInfo(defaultedFilters, queryInfo) ) } const findAll = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( filters: QueryInfoFilters = {} ): QueryInfo[] => { return Object.keys(filters).length ? getAll().filter(queryInfo => matchQueryInfo(filters, queryInfo)) : getAll() } const notify = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( event: QueryCacheNotifyEvent ) => { lastUpdated = Date.now() listeners.forEach(listener => listener(event)) } const remove = (queryInfo: QueryInfo) => { const queryInfoInMap = queries.get(queryInfo.queryHash) if (queryInfoInMap) { queryInfoInMap.destroy() if (queryInfoInMap === queryInfo) { queries.delete(queryInfo.queryHash) } notify({ type: 'removed', queryInfo }) } } const build = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( client: QueryClient, options: QueryInfoOptions, state?: QueryInfoState ): QueryInfo => { const queryHash = options.queryHash ?? hashKeyByOptions( getFullKey(options.query.key, options.variables), options ) let queryInfo = get(queryHash) if (!queryInfo) { queryInfo = createQueryInfo({ query: options.query, variables: options.variables!, options: client.defaultQueryOptions(options), cache: queryCache, queryHash, state, }) queries.set(queryHash, queryInfo) notify({ type: 'added', queryInfo, }) } return queryInfo } const get = < TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData >( queryHash: string ): QueryInfo | undefined => { return queries.get(queryHash) } const onFocus = (): void => { getAll().forEach(queryInfo => queryInfo.onFocus()) } const onOnline = (): void => { getAll().forEach(queryInfo => queryInfo.onOnline()) } const clear = (): void => { getAll().forEach(query => remove(query)) } const queryCache = { build, getAll, find, findAll, remove, get, onFocus, onOnline, subscribe, notify, clear, config, get lastUpdated() { return lastUpdated }, } return queryCache } export type QueryInfoTypeFilter = 'all' | 'active' | 'inactive' export interface QueryInfoFilters< TFetcherData = unknown, TVars = unknown, TError = Error, TQueryData = TFetcherData > { /** * Filter to active queries, inactive queries or all queries */ type?: QueryInfoTypeFilter /** * Match query key exactly */ exact?: boolean /** * Include queries matching this predicate function */ predicate?: ( queryInfo: QueryInfo ) => boolean query?: PrimitiveQuery variables?: DeepPartial fetchStatus?: FetchStatus stale?: boolean } const matchQueryInfo = ( filters: QueryInfoFilters, queryInfo: QueryInfo ): boolean => { const { type = 'all', exact, predicate, query, variables, stale, fetchStatus, } = filters if (query) { if (exact) { if ( queryInfo.queryHash !== hashKeyByOptions(getFullKey(query.key, variables), queryInfo.options) ) { return false } } else if ( !partialMatchKey( getFullKey(queryInfo.query.key, queryInfo.variables), getFullKey(query.key, variables) ) ) { return false } } if (type !== 'all') { const isActive = queryInfo.isActive() if (type === 'active' && !isActive) { return false } if (type === 'inactive' && isActive) { return false } } if (isBoolean(stale) && queryInfo.isStale() !== stale) { return false } if ( !isUndefined(fetchStatus) && fetchStatus !== queryInfo.state.fetchStatus ) { return false } if (predicate && !predicate(queryInfo)) { return false } return true }