UNPKG

8.35 kBPlain TextView Raw
1import type { createSelector as _createSelector } from './rtkImports'
2import { createNextState } from './rtkImports'
3import type {
4 MutationSubState,
5 QuerySubState,
6 RootState as _RootState,
7 RequestStatusFlags,
8 QueryCacheKey,
9 QueryKeys,
10 QueryState,
11} from './apiState'
12import { QueryStatus, getRequestStatusFlags } from './apiState'
13import type {
14 EndpointDefinitions,
15 QueryDefinition,
16 MutationDefinition,
17 QueryArgFrom,
18 TagTypesFrom,
19 ReducerPathFrom,
20 TagDescription,
21} from '../endpointDefinitions'
22import { expandTagDescription } from '../endpointDefinitions'
23import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
24import { getMutationCacheKey } from './buildSlice'
25import { flatten } from '../utils'
26
27export type SkipToken = typeof skipToken
28/**
29 * Can be passed into `useQuery`, `useQueryState` or `useQuerySubscription`
30 * instead of the query argument to get the same effect as if setting
31 * `skip: true` in the query options.
32 *
33 * Useful for scenarios where a query should be skipped when `arg` is `undefined`
34 * and TypeScript complains about it because `arg` is not allowed to be passed
35 * in as `undefined`, such as
36 *
37 * ```ts
38 * // codeblock-meta title="will error if the query argument is not allowed to be undefined" no-transpile
39 * useSomeQuery(arg, { skip: !!arg })
40 * ```
41 *
42 * ```ts
43 * // codeblock-meta title="using skipToken instead" no-transpile
44 * useSomeQuery(arg ?? skipToken)
45 * ```
46 *
47 * If passed directly into a query or mutation selector, that selector will always
48 * return an uninitialized state.
49 */
50export const skipToken = /* @__PURE__ */ Symbol.for('RTKQ/skipToken')
51
52declare module './module' {
53 export interface ApiEndpointQuery<
54 Definition extends QueryDefinition<any, any, any, any, any>,
55 Definitions extends EndpointDefinitions,
56 > {
57 select: QueryResultSelectorFactory<
58 Definition,
59 _RootState<
60 Definitions,
61 TagTypesFrom<Definition>,
62 ReducerPathFrom<Definition>
63 >
64 >
65 }
66
67 export interface ApiEndpointMutation<
68 Definition extends MutationDefinition<any, any, any, any, any>,
69 Definitions extends EndpointDefinitions,
70 > {
71 select: MutationResultSelectorFactory<
72 Definition,
73 _RootState<
74 Definitions,
75 TagTypesFrom<Definition>,
76 ReducerPathFrom<Definition>
77 >
78 >
79 }
80}
81
82type QueryResultSelectorFactory<
83 Definition extends QueryDefinition<any, any, any, any>,
84 RootState,
85> = (
86 queryArg: QueryArgFrom<Definition> | SkipToken,
87) => (state: RootState) => QueryResultSelectorResult<Definition>
88
89export type QueryResultSelectorResult<
90 Definition extends QueryDefinition<any, any, any, any>,
91> = QuerySubState<Definition> & RequestStatusFlags
92
93type MutationResultSelectorFactory<
94 Definition extends MutationDefinition<any, any, any, any>,
95 RootState,
96> = (
97 requestId:
98 | string
99 | { requestId: string | undefined; fixedCacheKey: string | undefined }
100 | SkipToken,
101) => (state: RootState) => MutationResultSelectorResult<Definition>
102
103export type MutationResultSelectorResult<
104 Definition extends MutationDefinition<any, any, any, any>,
105> = MutationSubState<Definition> & RequestStatusFlags
106
107const initialSubState: QuerySubState<any> = {
108 status: QueryStatus.uninitialized as const,
109}
110
111// abuse immer to freeze default states
112const defaultQuerySubState = /* @__PURE__ */ createNextState(
113 initialSubState,
114 () => {},
115)
116const defaultMutationSubState = /* @__PURE__ */ createNextState(
117 initialSubState as MutationSubState<any>,
118 () => {},
119)
120
121export function buildSelectors<
122 Definitions extends EndpointDefinitions,
123 ReducerPath extends string,
124>({
125 serializeQueryArgs,
126 reducerPath,
127 createSelector,
128}: {
129 serializeQueryArgs: InternalSerializeQueryArgs
130 reducerPath: ReducerPath
131 createSelector: typeof _createSelector
132}) {
133 type RootState = _RootState<Definitions, string, string>
134
135 const selectSkippedQuery = (state: RootState) => defaultQuerySubState
136 const selectSkippedMutation = (state: RootState) => defaultMutationSubState
137
138 return {
139 buildQuerySelector,
140 buildMutationSelector,
141 selectInvalidatedBy,
142 selectCachedArgsForQuery,
143 }
144
145 function withRequestFlags<T extends { status: QueryStatus }>(
146 substate: T,
147 ): T & RequestStatusFlags {
148 return {
149 ...substate,
150 ...getRequestStatusFlags(substate.status),
151 }
152 }
153
154 function selectInternalState(rootState: RootState) {
155 const state = rootState[reducerPath]
156 if (process.env.NODE_ENV !== 'production') {
157 if (!state) {
158 if ((selectInternalState as any).triggered) return state
159 ;(selectInternalState as any).triggered = true
160 console.error(
161 `Error: No data found at \`state.${reducerPath}\`. Did you forget to add the reducer to the store?`,
162 )
163 }
164 }
165 return state
166 }
167
168 function buildQuerySelector(
169 endpointName: string,
170 endpointDefinition: QueryDefinition<any, any, any, any>,
171 ) {
172 return ((queryArgs: any) => {
173 const serializedArgs = serializeQueryArgs({
174 queryArgs,
175 endpointDefinition,
176 endpointName,
177 })
178 const selectQuerySubstate = (state: RootState) =>
179 selectInternalState(state)?.queries?.[serializedArgs] ??
180 defaultQuerySubState
181 const finalSelectQuerySubState =
182 queryArgs === skipToken ? selectSkippedQuery : selectQuerySubstate
183
184 return createSelector(finalSelectQuerySubState, withRequestFlags)
185 }) as QueryResultSelectorFactory<any, RootState>
186 }
187
188 function buildMutationSelector() {
189 return ((id) => {
190 let mutationId: string | typeof skipToken
191 if (typeof id === 'object') {
192 mutationId = getMutationCacheKey(id) ?? skipToken
193 } else {
194 mutationId = id
195 }
196 const selectMutationSubstate = (state: RootState) =>
197 selectInternalState(state)?.mutations?.[mutationId as string] ??
198 defaultMutationSubState
199 const finalSelectMutationSubstate =
200 mutationId === skipToken
201 ? selectSkippedMutation
202 : selectMutationSubstate
203
204 return createSelector(finalSelectMutationSubstate, withRequestFlags)
205 }) as MutationResultSelectorFactory<any, RootState>
206 }
207
208 function selectInvalidatedBy(
209 state: RootState,
210 tags: ReadonlyArray<TagDescription<string>>,
211 ): Array<{
212 endpointName: string
213 originalArgs: any
214 queryCacheKey: QueryCacheKey
215 }> {
216 const apiState = state[reducerPath]
217 const toInvalidate = new Set<QueryCacheKey>()
218 for (const tag of tags.map(expandTagDescription)) {
219 const provided = apiState.provided[tag.type]
220 if (!provided) {
221 continue
222 }
223
224 let invalidateSubscriptions =
225 (tag.id !== undefined
226 ? // id given: invalidate all queries that provide this type & id
227 provided[tag.id]
228 : // no id: invalidate all queries that provide this type
229 flatten(Object.values(provided))) ?? []
230
231 for (const invalidate of invalidateSubscriptions) {
232 toInvalidate.add(invalidate)
233 }
234 }
235
236 return flatten(
237 Array.from(toInvalidate.values()).map((queryCacheKey) => {
238 const querySubState = apiState.queries[queryCacheKey]
239 return querySubState
240 ? [
241 {
242 queryCacheKey,
243 endpointName: querySubState.endpointName!,
244 originalArgs: querySubState.originalArgs,
245 },
246 ]
247 : []
248 }),
249 )
250 }
251
252 function selectCachedArgsForQuery<QueryName extends QueryKeys<Definitions>>(
253 state: RootState,
254 queryName: QueryName,
255 ): Array<QueryArgFrom<Definitions[QueryName]>> {
256 return Object.values(state[reducerPath].queries as QueryState<any>)
257 .filter(
258 (
259 entry,
260 ): entry is Exclude<
261 QuerySubState<Definitions[QueryName]>,
262 { status: QueryStatus.uninitialized }
263 > =>
264 entry?.endpointName === queryName &&
265 entry.status !== QueryStatus.uninitialized,
266 )
267 .map((entry) => entry.originalArgs)
268 }
269}
270
\No newline at end of file