import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import type { EndpointDefinition, EndpointDefinitions, InfiniteQueryArgFrom, InfiniteQueryDefinition, MutationDefinition, QueryArgFrom, QueryArgFromAnyQuery, QueryDefinition, ReducerPathFrom, TagDescription, TagTypesFrom, } from '../endpointDefinitions' import { expandTagDescription } from '../endpointDefinitions' import { filterMap, isNotNullish } from '../utils' import type { InfiniteData, InfiniteQueryConfigOptions, InfiniteQuerySubState, MutationSubState, QueryCacheKey, QueryState, QuerySubState, RequestStatusFlags, RootState as _RootState, QueryStatus, } from './apiState' import { STATUS_UNINITIALIZED, getRequestStatusFlags } from './apiState' import { getMutationCacheKey } from './buildSlice' import type { createSelector as _createSelector } from './rtkImports' import { createNextState } from './rtkImports' import { type AllQueryKeys, getNextPageParam, getPreviousPageParam, } from './buildThunks' export type SkipToken = typeof skipToken /** * Can be passed into `useQuery`, `useQueryState` or `useQuerySubscription` * instead of the query argument to get the same effect as if setting * `skip: true` in the query options. * * Useful for scenarios where a query should be skipped when `arg` is `undefined` * and TypeScript complains about it because `arg` is not allowed to be passed * in as `undefined`, such as * * ```ts * // codeblock-meta title="will error if the query argument is not allowed to be undefined" no-transpile * useSomeQuery(arg, { skip: !!arg }) * ``` * * ```ts * // codeblock-meta title="using skipToken instead" no-transpile * useSomeQuery(arg ?? skipToken) * ``` * * If passed directly into a query or mutation selector, that selector will always * return an uninitialized state. */ export const skipToken = /* @__PURE__ */ Symbol.for('RTKQ/skipToken') export type BuildSelectorsApiEndpointQuery< Definition extends QueryDefinition, Definitions extends EndpointDefinitions, > = { select: QueryResultSelectorFactory< Definition, _RootState< Definitions, TagTypesFrom, ReducerPathFrom > > } export type BuildSelectorsApiEndpointInfiniteQuery< Definition extends InfiniteQueryDefinition, Definitions extends EndpointDefinitions, > = { select: InfiniteQueryResultSelectorFactory< Definition, _RootState< Definitions, TagTypesFrom, ReducerPathFrom > > } export type BuildSelectorsApiEndpointMutation< Definition extends MutationDefinition, Definitions extends EndpointDefinitions, > = { select: MutationResultSelectorFactory< Definition, _RootState< Definitions, TagTypesFrom, ReducerPathFrom > > } type QueryResultSelectorFactory< Definition extends QueryDefinition, RootState, > = ( queryArg: QueryArgFrom | SkipToken, ) => (state: RootState) => QueryResultSelectorResult export type QueryResultSelectorResult< Definition extends QueryDefinition, > = QuerySubState & RequestStatusFlags type InfiniteQueryResultSelectorFactory< Definition extends InfiniteQueryDefinition, RootState, > = ( queryArg: InfiniteQueryArgFrom | SkipToken, ) => (state: RootState) => InfiniteQueryResultSelectorResult export type InfiniteQueryResultFlags = { hasNextPage: boolean hasPreviousPage: boolean isFetchingNextPage: boolean isFetchingPreviousPage: boolean isFetchNextPageError: boolean isFetchPreviousPageError: boolean } export type InfiniteQueryResultSelectorResult< Definition extends InfiniteQueryDefinition, > = InfiniteQuerySubState & RequestStatusFlags & InfiniteQueryResultFlags type MutationResultSelectorFactory< Definition extends MutationDefinition, RootState, > = ( requestId: | string | { requestId: string | undefined; fixedCacheKey: string | undefined } | SkipToken, ) => (state: RootState) => MutationResultSelectorResult export type MutationResultSelectorResult< Definition extends MutationDefinition, > = MutationSubState & RequestStatusFlags const initialSubState: QuerySubState = { status: STATUS_UNINITIALIZED, } // abuse immer to freeze default states const defaultQuerySubState = /* @__PURE__ */ createNextState( initialSubState, () => {}, ) const defaultMutationSubState = /* @__PURE__ */ createNextState( initialSubState as MutationSubState, () => {}, ) export type AllSelectors = ReturnType export function buildSelectors< Definitions extends EndpointDefinitions, ReducerPath extends string, >({ serializeQueryArgs, reducerPath, createSelector, }: { serializeQueryArgs: InternalSerializeQueryArgs reducerPath: ReducerPath createSelector: typeof _createSelector }) { type RootState = _RootState const selectSkippedQuery = (state: RootState) => defaultQuerySubState const selectSkippedMutation = (state: RootState) => defaultMutationSubState return { buildQuerySelector, buildInfiniteQuerySelector, buildMutationSelector, selectInvalidatedBy, selectCachedArgsForQuery, selectApiState, selectQueries, selectMutations, selectQueryEntry, selectConfig, } function withRequestFlags( substate: T, ): T & RequestStatusFlags { return { ...substate, ...getRequestStatusFlags(substate.status) } } function selectApiState(rootState: RootState) { const state = rootState[reducerPath] if (process.env.NODE_ENV !== 'production') { if (!state) { if ((selectApiState as any).triggered) return state ;(selectApiState as any).triggered = true console.error( `Error: No data found at \`state.${reducerPath}\`. Did you forget to add the reducer to the store?`, ) } } return state } function selectQueries(rootState: RootState) { return selectApiState(rootState)?.queries } function selectQueryEntry(rootState: RootState, cacheKey: QueryCacheKey) { return selectQueries(rootState)?.[cacheKey] } function selectMutations(rootState: RootState) { return selectApiState(rootState)?.mutations } function selectConfig(rootState: RootState) { return selectApiState(rootState)?.config } function buildAnyQuerySelector( endpointName: string, endpointDefinition: EndpointDefinition, combiner: ( substate: T, ) => T & RequestStatusFlags, ) { return (queryArgs: any) => { // Avoid calling serializeQueryArgs if the arg is skipToken if (queryArgs === skipToken) { return createSelector(selectSkippedQuery, combiner) } const serializedArgs = serializeQueryArgs({ queryArgs, endpointDefinition, endpointName, }) const selectQuerySubstate = (state: RootState) => selectQueryEntry(state, serializedArgs) ?? defaultQuerySubState return createSelector(selectQuerySubstate, combiner) } } function buildQuerySelector( endpointName: string, endpointDefinition: QueryDefinition, ) { return buildAnyQuerySelector( endpointName, endpointDefinition, withRequestFlags, ) as QueryResultSelectorFactory } function buildInfiniteQuerySelector( endpointName: string, endpointDefinition: InfiniteQueryDefinition, ) { const { infiniteQueryOptions } = endpointDefinition function withInfiniteQueryResultFlags( substate: T, ): T & RequestStatusFlags & InfiniteQueryResultFlags { const stateWithRequestFlags = { ...(substate as InfiniteQuerySubState), ...getRequestStatusFlags(substate.status), } const { isLoading, isError, direction } = stateWithRequestFlags const isForward = direction === 'forward' const isBackward = direction === 'backward' return { ...stateWithRequestFlags, hasNextPage: getHasNextPage( infiniteQueryOptions, stateWithRequestFlags.data, stateWithRequestFlags.originalArgs, ), hasPreviousPage: getHasPreviousPage( infiniteQueryOptions, stateWithRequestFlags.data, stateWithRequestFlags.originalArgs, ), isFetchingNextPage: isLoading && isForward, isFetchingPreviousPage: isLoading && isBackward, isFetchNextPageError: isError && isForward, isFetchPreviousPageError: isError && isBackward, } } return buildAnyQuerySelector( endpointName, endpointDefinition, withInfiniteQueryResultFlags, ) as unknown as InfiniteQueryResultSelectorFactory } function buildMutationSelector() { return ((id) => { let mutationId: string | typeof skipToken if (typeof id === 'object') { mutationId = getMutationCacheKey(id) ?? skipToken } else { mutationId = id } const selectMutationSubstate = (state: RootState) => selectApiState(state)?.mutations?.[mutationId as string] ?? defaultMutationSubState const finalSelectMutationSubstate = mutationId === skipToken ? selectSkippedMutation : selectMutationSubstate return createSelector(finalSelectMutationSubstate, withRequestFlags) }) as MutationResultSelectorFactory } function selectInvalidatedBy( state: RootState, tags: ReadonlyArray | null | undefined>, ): Array<{ endpointName: string originalArgs: any queryCacheKey: QueryCacheKey }> { const apiState = state[reducerPath] const toInvalidate = new Set() const finalTags = filterMap(tags, isNotNullish, expandTagDescription) for (const tag of finalTags) { const provided = apiState.provided.tags[tag.type] if (!provided) { continue } let invalidateSubscriptions = (tag.id !== undefined ? // id given: invalidate all queries that provide this type & id provided[tag.id] : // no id: invalidate all queries that provide this type Object.values(provided).flat()) ?? [] for (const invalidate of invalidateSubscriptions) { toInvalidate.add(invalidate) } } return Array.from(toInvalidate.values()).flatMap((queryCacheKey) => { const querySubState = apiState.queries[queryCacheKey] return querySubState ? { queryCacheKey, endpointName: querySubState.endpointName!, originalArgs: querySubState.originalArgs, } : [] }) } function selectCachedArgsForQuery< QueryName extends AllQueryKeys, >( state: RootState, queryName: QueryName, ): Array> { return filterMap( Object.values(selectQueries(state) as QueryState), ( entry, ): entry is Exclude< QuerySubState, { status: QueryStatus.uninitialized } > => entry?.endpointName === queryName && entry.status !== STATUS_UNINITIALIZED, (entry) => entry.originalArgs, ) } function getHasNextPage( options: InfiniteQueryConfigOptions, data?: InfiniteData, queryArg?: unknown, ): boolean { if (!data) return false return getNextPageParam(options, data, queryArg) != null } function getHasPreviousPage( options: InfiniteQueryConfigOptions, data?: InfiniteData, queryArg?: unknown, ): boolean { if (!data || !options.getPreviousPageParam) return false return getPreviousPageParam(options, data, queryArg) != null } }