UNPKG

14.4 kBPlain TextView Raw
1import type {
2 EndpointDefinitions,
3 QueryDefinition,
4 MutationDefinition,
5 QueryArgFrom,
6 ResultTypeFrom,
7} from '../endpointDefinitions'
8import { DefinitionType, isQueryDefinition } from '../endpointDefinitions'
9import type { QueryThunk, MutationThunk, QueryThunkArg } from './buildThunks'
10import type {
11 UnknownAction,
12 ThunkAction,
13 SerializedError,
14} from '@reduxjs/toolkit'
15import type { SubscriptionOptions, RootState } from './apiState'
16import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
17import type { Api, ApiContext } from '../apiTypes'
18import type { ApiEndpointQuery } from './module'
19import type { BaseQueryError, QueryReturnValue } from '../baseQueryTypes'
20import type { QueryResultSelectorResult } from './buildSelectors'
21import type { Dispatch } from 'redux'
22import { isNotNullish } from '../utils/isNotNullish'
23import { countObjectKeys } from '../utils/countObjectKeys'
24import type { SafePromise } from '../../tsHelpers'
25import { asSafePromise } from '../../tsHelpers'
26
27declare module './module' {
28 export interface ApiEndpointQuery<
29 Definition extends QueryDefinition<any, any, any, any, any>,
30 // eslint-disable-next-line @typescript-eslint/no-unused-vars
31 Definitions extends EndpointDefinitions,
32 > {
33 initiate: StartQueryActionCreator<Definition>
34 }
35
36 export interface ApiEndpointMutation<
37 Definition extends MutationDefinition<any, any, any, any, any>,
38 // eslint-disable-next-line @typescript-eslint/no-unused-vars
39 Definitions extends EndpointDefinitions,
40 > {
41 initiate: StartMutationActionCreator<Definition>
42 }
43}
44
45export const forceQueryFnSymbol = Symbol('forceQueryFn')
46export const isUpsertQuery = (arg: QueryThunkArg) =>
47 typeof arg[forceQueryFnSymbol] === 'function'
48
49export interface StartQueryActionCreatorOptions {
50 subscribe?: boolean
51 forceRefetch?: boolean | number
52 subscriptionOptions?: SubscriptionOptions
53 [forceQueryFnSymbol]?: () => QueryReturnValue
54}
55
56type StartQueryActionCreator<
57 D extends QueryDefinition<any, any, any, any, any>,
58> = (
59 arg: QueryArgFrom<D>,
60 options?: StartQueryActionCreatorOptions,
61) => ThunkAction<QueryActionCreatorResult<D>, any, any, UnknownAction>
62
63export type QueryActionCreatorResult<
64 D extends QueryDefinition<any, any, any, any>,
65> = SafePromise<QueryResultSelectorResult<D>> & {
66 arg: QueryArgFrom<D>
67 requestId: string
68 subscriptionOptions: SubscriptionOptions | undefined
69 abort(): void
70 unwrap(): Promise<ResultTypeFrom<D>>
71 unsubscribe(): void
72 refetch(): QueryActionCreatorResult<D>
73 updateSubscriptionOptions(options: SubscriptionOptions): void
74 queryCacheKey: string
75}
76
77type StartMutationActionCreator<
78 D extends MutationDefinition<any, any, any, any>,
79> = (
80 arg: QueryArgFrom<D>,
81 options?: {
82 /**
83 * If this mutation should be tracked in the store.
84 * If you just want to manually trigger this mutation using `dispatch` and don't care about the
85 * result, state & potential errors being held in store, you can set this to false.
86 * (defaults to `true`)
87 */
88 track?: boolean
89 fixedCacheKey?: string
90 },
91) => ThunkAction<MutationActionCreatorResult<D>, any, any, UnknownAction>
92
93export type MutationActionCreatorResult<
94 D extends MutationDefinition<any, any, any, any>,
95> = SafePromise<
96 | {
97 data: ResultTypeFrom<D>
98 error?: undefined
99 }
100 | {
101 data?: undefined
102 error:
103 | Exclude<
104 BaseQueryError<
105 D extends MutationDefinition<any, infer BaseQuery, any, any>
106 ? BaseQuery
107 : never
108 >,
109 undefined
110 >
111 | SerializedError
112 }
113> & {
114 /** @internal */
115 arg: {
116 /**
117 * The name of the given endpoint for the mutation
118 */
119 endpointName: string
120 /**
121 * The original arguments supplied to the mutation call
122 */
123 originalArgs: QueryArgFrom<D>
124 /**
125 * Whether the mutation is being tracked in the store.
126 */
127 track?: boolean
128 fixedCacheKey?: string
129 }
130 /**
131 * A unique string generated for the request sequence
132 */
133 requestId: string
134
135 /**
136 * A method to cancel the mutation promise. Note that this is not intended to prevent the mutation
137 * that was fired off from reaching the server, but only to assist in handling the response.
138 *
139 * Calling `abort()` prior to the promise resolving will force it to reach the error state with
140 * the serialized error:
141 * `{ name: 'AbortError', message: 'Aborted' }`
142 *
143 * @example
144 * ```ts
145 * const [updateUser] = useUpdateUserMutation();
146 *
147 * useEffect(() => {
148 * const promise = updateUser(id);
149 * promise
150 * .unwrap()
151 * .catch((err) => {
152 * if (err.name === 'AbortError') return;
153 * // else handle the unexpected error
154 * })
155 *
156 * return () => {
157 * promise.abort();
158 * }
159 * }, [id, updateUser])
160 * ```
161 */
162 abort(): void
163 /**
164 * Unwraps a mutation call to provide the raw response/error.
165 *
166 * @remarks
167 * If you need to access the error or success payload immediately after a mutation, you can chain .unwrap().
168 *
169 * @example
170 * ```ts
171 * // codeblock-meta title="Using .unwrap"
172 * addPost({ id: 1, name: 'Example' })
173 * .unwrap()
174 * .then((payload) => console.log('fulfilled', payload))
175 * .catch((error) => console.error('rejected', error));
176 * ```
177 *
178 * @example
179 * ```ts
180 * // codeblock-meta title="Using .unwrap with async await"
181 * try {
182 * const payload = await addPost({ id: 1, name: 'Example' }).unwrap();
183 * console.log('fulfilled', payload)
184 * } catch (error) {
185 * console.error('rejected', error);
186 * }
187 * ```
188 */
189 unwrap(): Promise<ResultTypeFrom<D>>
190 /**
191 * A method to manually unsubscribe from the mutation call, meaning it will be removed from cache after the usual caching grace period.
192 The value returned by the hook will reset to `isUninitialized` afterwards.
193 */
194 reset(): void
195}
196
197export function buildInitiate({
198 serializeQueryArgs,
199 queryThunk,
200 mutationThunk,
201 api,
202 context,
203}: {
204 serializeQueryArgs: InternalSerializeQueryArgs
205 queryThunk: QueryThunk
206 mutationThunk: MutationThunk
207 api: Api<any, EndpointDefinitions, any, any>
208 context: ApiContext<EndpointDefinitions>
209}) {
210 const runningQueries: Map<
211 Dispatch,
212 Record<string, QueryActionCreatorResult<any> | undefined>
213 > = new Map()
214 const runningMutations: Map<
215 Dispatch,
216 Record<string, MutationActionCreatorResult<any> | undefined>
217 > = new Map()
218
219 const {
220 unsubscribeQueryResult,
221 removeMutationResult,
222 updateSubscriptionOptions,
223 } = api.internalActions
224 return {
225 buildInitiateQuery,
226 buildInitiateMutation,
227 getRunningQueryThunk,
228 getRunningMutationThunk,
229 getRunningQueriesThunk,
230 getRunningMutationsThunk,
231 }
232
233 function getRunningQueryThunk(endpointName: string, queryArgs: any) {
234 return (dispatch: Dispatch) => {
235 const endpointDefinition = context.endpointDefinitions[endpointName]
236 const queryCacheKey = serializeQueryArgs({
237 queryArgs,
238 endpointDefinition,
239 endpointName,
240 })
241 return runningQueries.get(dispatch)?.[queryCacheKey] as
242 | QueryActionCreatorResult<never>
243 | undefined
244 }
245 }
246
247 function getRunningMutationThunk(
248 /**
249 * this is only here to allow TS to infer the result type by input value
250 * we could use it to validate the result, but it's probably not necessary
251 */
252 _endpointName: string,
253 fixedCacheKeyOrRequestId: string,
254 ) {
255 return (dispatch: Dispatch) => {
256 return runningMutations.get(dispatch)?.[fixedCacheKeyOrRequestId] as
257 | MutationActionCreatorResult<never>
258 | undefined
259 }
260 }
261
262 function getRunningQueriesThunk() {
263 return (dispatch: Dispatch) =>
264 Object.values(runningQueries.get(dispatch) || {}).filter(isNotNullish)
265 }
266
267 function getRunningMutationsThunk() {
268 return (dispatch: Dispatch) =>
269 Object.values(runningMutations.get(dispatch) || {}).filter(isNotNullish)
270 }
271
272 function middlewareWarning(dispatch: Dispatch) {
273 if (process.env.NODE_ENV !== 'production') {
274 if ((middlewareWarning as any).triggered) return
275 const returnedValue = dispatch(
276 api.internalActions.internal_getRTKQSubscriptions(),
277 )
278
279 ;(middlewareWarning as any).triggered = true
280
281 // The RTKQ middleware should return the internal state object,
282 // but it should _not_ be the action object.
283 if (
284 typeof returnedValue !== 'object' ||
285 typeof returnedValue?.type === 'string'
286 ) {
287 // Otherwise, must not have been added
288 throw new Error(
289 `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
290You must add the middleware for RTK-Query to function correctly!`,
291 )
292 }
293 }
294 }
295
296 function buildInitiateQuery(
297 endpointName: string,
298 endpointDefinition: QueryDefinition<any, any, any, any>,
299 ) {
300 const queryAction: StartQueryActionCreator<any> =
301 (
302 arg,
303 {
304 subscribe = true,
305 forceRefetch,
306 subscriptionOptions,
307 [forceQueryFnSymbol]: forceQueryFn,
308 ...rest
309 } = {},
310 ) =>
311 (dispatch, getState) => {
312 const queryCacheKey = serializeQueryArgs({
313 queryArgs: arg,
314 endpointDefinition,
315 endpointName,
316 })
317
318 const thunk = queryThunk({
319 ...rest,
320 type: 'query',
321 subscribe,
322 forceRefetch: forceRefetch,
323 subscriptionOptions,
324 endpointName,
325 originalArgs: arg,
326 queryCacheKey,
327 [forceQueryFnSymbol]: forceQueryFn,
328 })
329 const selector = (
330 api.endpoints[endpointName] as ApiEndpointQuery<any, any>
331 ).select(arg)
332
333 const thunkResult = dispatch(thunk)
334 const stateAfter = selector(getState())
335
336 middlewareWarning(dispatch)
337
338 const { requestId, abort } = thunkResult
339
340 const skippedSynchronously = stateAfter.requestId !== requestId
341
342 const runningQuery = runningQueries.get(dispatch)?.[queryCacheKey]
343 const selectFromState = () => selector(getState())
344
345 const statePromise: QueryActionCreatorResult<any> = Object.assign(
346 (forceQueryFn
347 ? // a query has been forced (upsertQueryData)
348 // -> we want to resolve it once data has been written with the data that will be written
349 thunkResult.then(selectFromState)
350 : skippedSynchronously && !runningQuery
351 ? // a query has been skipped due to a condition and we do not have any currently running query
352 // -> we want to resolve it immediately with the current data
353 Promise.resolve(stateAfter)
354 : // query just started or one is already in flight
355 // -> wait for the running query, then resolve with data from after that
356 Promise.all([runningQuery, thunkResult]).then(
357 selectFromState,
358 )) as SafePromise<any>,
359 {
360 arg,
361 requestId,
362 subscriptionOptions,
363 queryCacheKey,
364 abort,
365 async unwrap() {
366 const result = await statePromise
367
368 if (result.isError) {
369 throw result.error
370 }
371
372 return result.data
373 },
374 refetch: () =>
375 dispatch(
376 queryAction(arg, { subscribe: false, forceRefetch: true }),
377 ),
378 unsubscribe() {
379 if (subscribe)
380 dispatch(
381 unsubscribeQueryResult({
382 queryCacheKey,
383 requestId,
384 }),
385 )
386 },
387 updateSubscriptionOptions(options: SubscriptionOptions) {
388 statePromise.subscriptionOptions = options
389 dispatch(
390 updateSubscriptionOptions({
391 endpointName,
392 requestId,
393 queryCacheKey,
394 options,
395 }),
396 )
397 },
398 },
399 )
400
401 if (!runningQuery && !skippedSynchronously && !forceQueryFn) {
402 const running = runningQueries.get(dispatch) || {}
403 running[queryCacheKey] = statePromise
404 runningQueries.set(dispatch, running)
405
406 statePromise.then(() => {
407 delete running[queryCacheKey]
408 if (!countObjectKeys(running)) {
409 runningQueries.delete(dispatch)
410 }
411 })
412 }
413
414 return statePromise
415 }
416 return queryAction
417 }
418
419 function buildInitiateMutation(
420 endpointName: string,
421 ): StartMutationActionCreator<any> {
422 return (arg, { track = true, fixedCacheKey } = {}) =>
423 (dispatch, getState) => {
424 const thunk = mutationThunk({
425 type: 'mutation',
426 endpointName,
427 originalArgs: arg,
428 track,
429 fixedCacheKey,
430 })
431 const thunkResult = dispatch(thunk)
432 middlewareWarning(dispatch)
433 const { requestId, abort, unwrap } = thunkResult
434 const returnValuePromise = asSafePromise(
435 thunkResult.unwrap().then((data) => ({ data })),
436 (error) => ({ error }),
437 )
438
439 const reset = () => {
440 dispatch(removeMutationResult({ requestId, fixedCacheKey }))
441 }
442
443 const ret = Object.assign(returnValuePromise, {
444 arg: thunkResult.arg,
445 requestId,
446 abort,
447 unwrap,
448 reset,
449 })
450
451 const running = runningMutations.get(dispatch) || {}
452 runningMutations.set(dispatch, running)
453 running[requestId] = ret
454 ret.then(() => {
455 delete running[requestId]
456 if (!countObjectKeys(running)) {
457 runningMutations.delete(dispatch)
458 }
459 })
460 if (fixedCacheKey) {
461 running[fixedCacheKey] = ret
462 ret.then(() => {
463 if (running[fixedCacheKey] === ret) {
464 delete running[fixedCacheKey]
465 if (!countObjectKeys(running)) {
466 runningMutations.delete(dispatch)
467 }
468 }
469 })
470 }
471
472 return ret
473 }
474 }
475}