import type { createSelector as _createSelector } from './rtkImports' import { createNextState } from './rtkImports' import type { MutationSubState, QuerySubState, RootState as _RootState, RequestStatusFlags, QueryCacheKey, QueryKeys, QueryState, } from './apiState' import { QueryStatus, getRequestStatusFlags } from './apiState' import type { EndpointDefinitions, QueryDefinition, MutationDefinition, QueryArgFrom, TagTypesFrom, ReducerPathFrom, TagDescription, } from '../endpointDefinitions' import { expandTagDescription } from '../endpointDefinitions' import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import { getMutationCacheKey } from './buildSlice' import { flatten } from '../utils' 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') declare module './module' { export interface ApiEndpointQuery< Definition extends QueryDefinition, Definitions extends EndpointDefinitions, > { select: QueryResultSelectorFactory< Definition, _RootState< Definitions, TagTypesFrom, ReducerPathFrom > > } export interface ApiEndpointMutation< 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 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: QueryStatus.uninitialized as const, } // abuse immer to freeze default states const defaultQuerySubState = /* @__PURE__ */ createNextState( initialSubState, () => {}, ) const defaultMutationSubState = /* @__PURE__ */ createNextState( initialSubState as MutationSubState, () => {}, ) 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, buildMutationSelector, selectInvalidatedBy, selectCachedArgsForQuery, } function withRequestFlags( substate: T, ): T & RequestStatusFlags { return { ...substate, ...getRequestStatusFlags(substate.status), } } function selectInternalState(rootState: RootState) { const state = rootState[reducerPath] if (process.env.NODE_ENV !== 'production') { if (!state) { if ((selectInternalState as any).triggered) return state ;(selectInternalState 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 buildQuerySelector( endpointName: string, endpointDefinition: QueryDefinition, ) { return ((queryArgs: any) => { const serializedArgs = serializeQueryArgs({ queryArgs, endpointDefinition, endpointName, }) const selectQuerySubstate = (state: RootState) => selectInternalState(state)?.queries?.[serializedArgs] ?? defaultQuerySubState const finalSelectQuerySubState = queryArgs === skipToken ? selectSkippedQuery : selectQuerySubstate return createSelector(finalSelectQuerySubState, withRequestFlags) }) as QueryResultSelectorFactory } function buildMutationSelector() { return ((id) => { let mutationId: string | typeof skipToken if (typeof id === 'object') { mutationId = getMutationCacheKey(id) ?? skipToken } else { mutationId = id } const selectMutationSubstate = (state: RootState) => selectInternalState(state)?.mutations?.[mutationId as string] ?? defaultMutationSubState const finalSelectMutationSubstate = mutationId === skipToken ? selectSkippedMutation : selectMutationSubstate return createSelector(finalSelectMutationSubstate, withRequestFlags) }) as MutationResultSelectorFactory } function selectInvalidatedBy( state: RootState, tags: ReadonlyArray>, ): Array<{ endpointName: string originalArgs: any queryCacheKey: QueryCacheKey }> { const apiState = state[reducerPath] const toInvalidate = new Set() for (const tag of tags.map(expandTagDescription)) { const provided = apiState.provided[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 flatten(Object.values(provided))) ?? [] for (const invalidate of invalidateSubscriptions) { toInvalidate.add(invalidate) } } return flatten( Array.from(toInvalidate.values()).map((queryCacheKey) => { const querySubState = apiState.queries[queryCacheKey] return querySubState ? [ { queryCacheKey, endpointName: querySubState.endpointName!, originalArgs: querySubState.originalArgs, }, ] : [] }), ) } function selectCachedArgsForQuery>( state: RootState, queryName: QueryName, ): Array> { return Object.values(state[reducerPath].queries as QueryState) .filter( ( entry, ): entry is Exclude< QuerySubState, { status: QueryStatus.uninitialized } > => entry?.endpointName === queryName && entry.status !== QueryStatus.uninitialized, ) .map((entry) => entry.originalArgs) } }