source: node_modules/@reduxjs/toolkit/src/query/core/buildThunks.ts

Last change on this file was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 34.5 KB
Line 
1import type {
2 AsyncThunk,
3 AsyncThunkPayloadCreator,
4 Draft,
5 ThunkAction,
6 ThunkDispatch,
7 UnknownAction,
8} from '@reduxjs/toolkit'
9import type { Patch } from 'immer'
10import { isDraftable, produceWithPatches } from '../utils/immerImports'
11import type { Api, ApiContext } from '../apiTypes'
12import type {
13 BaseQueryError,
14 BaseQueryFn,
15 QueryReturnValue,
16} from '../baseQueryTypes'
17import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
18import type {
19 AssertTagTypes,
20 EndpointDefinition,
21 EndpointDefinitions,
22 InfiniteQueryArgFrom,
23 InfiniteQueryCombinedArg,
24 InfiniteQueryDefinition,
25 MutationDefinition,
26 PageParamFrom,
27 QueryArgFrom,
28 QueryDefinition,
29 ResultDescription,
30 ResultTypeFrom,
31 SchemaFailureConverter,
32 SchemaFailureHandler,
33 SchemaFailureInfo,
34 SchemaType,
35} from '../endpointDefinitions'
36import {
37 calculateProvidedBy,
38 ENDPOINT_QUERY,
39 isInfiniteQueryDefinition,
40 isQueryDefinition,
41} from '../endpointDefinitions'
42import { HandledError } from '../HandledError'
43import type { UnwrapPromise } from '../tsHelpers'
44import type {
45 RootState,
46 QueryKeys,
47 QuerySubstateIdentifier,
48 InfiniteData,
49 InfiniteQueryConfigOptions,
50 QueryCacheKey,
51 InfiniteQueryDirection,
52 InfiniteQueryKeys,
53} from './apiState'
54import { QueryStatus, STATUS_UNINITIALIZED } from './apiState'
55import type {
56 InfiniteQueryActionCreatorResult,
57 QueryActionCreatorResult,
58 StartInfiniteQueryActionCreatorOptions,
59 StartQueryActionCreatorOptions,
60} from './buildInitiate'
61import { forceQueryFnSymbol, isUpsertQuery } from './buildInitiate'
62import type { AllSelectors } from './buildSelectors'
63import type { ApiEndpointQuery, PrefetchOptions } from './module'
64import {
65 createAsyncThunk,
66 isAllOf,
67 isFulfilled,
68 isPending,
69 isRejected,
70 isRejectedWithValue,
71 SHOULD_AUTOBATCH,
72} from './rtkImports'
73import {
74 parseWithSchema,
75 NamedSchemaError,
76 shouldSkip,
77} from '../standardSchema'
78
79export type BuildThunksApiEndpointQuery<
80 Definition extends QueryDefinition<any, any, any, any, any>,
81> = Matchers<QueryThunk, Definition>
82
83export type BuildThunksApiEndpointInfiniteQuery<
84 Definition extends InfiniteQueryDefinition<any, any, any, any, any>,
85> = Matchers<InfiniteQueryThunk<any>, Definition>
86
87export type BuildThunksApiEndpointMutation<
88 Definition extends MutationDefinition<any, any, any, any, any>,
89> = Matchers<MutationThunk, Definition>
90
91type EndpointThunk<
92 Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
93 Definition extends EndpointDefinition<any, any, any, any>,
94> =
95 Definition extends EndpointDefinition<
96 infer QueryArg,
97 infer BaseQueryFn,
98 any,
99 infer ResultType
100 >
101 ? Thunk extends AsyncThunk<unknown, infer ATArg, infer ATConfig>
102 ? AsyncThunk<
103 ResultType,
104 ATArg & { originalArgs: QueryArg },
105 ATConfig & { rejectValue: BaseQueryError<BaseQueryFn> }
106 >
107 : never
108 : Definition extends InfiniteQueryDefinition<
109 infer QueryArg,
110 infer PageParam,
111 infer BaseQueryFn,
112 any,
113 infer ResultType
114 >
115 ? Thunk extends AsyncThunk<unknown, infer ATArg, infer ATConfig>
116 ? AsyncThunk<
117 InfiniteData<ResultType, PageParam>,
118 ATArg & { originalArgs: QueryArg },
119 ATConfig & { rejectValue: BaseQueryError<BaseQueryFn> }
120 >
121 : never
122 : never
123
124export type PendingAction<
125 Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
126 Definition extends EndpointDefinition<any, any, any, any>,
127> = ReturnType<EndpointThunk<Thunk, Definition>['pending']>
128
129export type FulfilledAction<
130 Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
131 Definition extends EndpointDefinition<any, any, any, any>,
132> = ReturnType<EndpointThunk<Thunk, Definition>['fulfilled']>
133
134export type RejectedAction<
135 Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
136 Definition extends EndpointDefinition<any, any, any, any>,
137> = ReturnType<EndpointThunk<Thunk, Definition>['rejected']>
138
139export type Matcher<M> = (value: any) => value is M
140
141export interface Matchers<
142 Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
143 Definition extends EndpointDefinition<any, any, any, any>,
144> {
145 matchPending: Matcher<PendingAction<Thunk, Definition>>
146 matchFulfilled: Matcher<FulfilledAction<Thunk, Definition>>
147 matchRejected: Matcher<RejectedAction<Thunk, Definition>>
148}
149
150export type QueryThunkArg = QuerySubstateIdentifier &
151 StartQueryActionCreatorOptions & {
152 type: 'query'
153 originalArgs: unknown
154 endpointName: string
155 }
156
157export type InfiniteQueryThunkArg<
158 D extends InfiniteQueryDefinition<any, any, any, any, any>,
159> = QuerySubstateIdentifier &
160 StartInfiniteQueryActionCreatorOptions<D> & {
161 type: `query`
162 originalArgs: unknown
163 endpointName: string
164 param: unknown
165 direction?: InfiniteQueryDirection
166 refetchCachedPages?: boolean
167 }
168
169type MutationThunkArg = {
170 type: 'mutation'
171 originalArgs: unknown
172 endpointName: string
173 track?: boolean
174 fixedCacheKey?: string
175}
176
177export type ThunkResult = unknown
178
179export type ThunkApiMetaConfig = {
180 pendingMeta: { startedTimeStamp: number; [SHOULD_AUTOBATCH]: true }
181 fulfilledMeta: {
182 fulfilledTimeStamp: number
183 baseQueryMeta: unknown
184 [SHOULD_AUTOBATCH]: true
185 }
186 rejectedMeta: { baseQueryMeta: unknown; [SHOULD_AUTOBATCH]: true }
187}
188export type QueryThunk = AsyncThunk<
189 ThunkResult,
190 QueryThunkArg,
191 ThunkApiMetaConfig
192>
193export type InfiniteQueryThunk<
194 D extends InfiniteQueryDefinition<any, any, any, any, any>,
195> = AsyncThunk<ThunkResult, InfiniteQueryThunkArg<D>, ThunkApiMetaConfig>
196export type MutationThunk = AsyncThunk<
197 ThunkResult,
198 MutationThunkArg,
199 ThunkApiMetaConfig
200>
201
202function defaultTransformResponse(baseQueryReturnValue: unknown) {
203 return baseQueryReturnValue
204}
205
206export type MaybeDrafted<T> = T | Draft<T>
207export type Recipe<T> = (data: MaybeDrafted<T>) => void | MaybeDrafted<T>
208export type UpsertRecipe<T> = (
209 data: MaybeDrafted<T> | undefined,
210) => void | MaybeDrafted<T>
211
212export type PatchQueryDataThunk<
213 Definitions extends EndpointDefinitions,
214 PartialState,
215> = <EndpointName extends QueryKeys<Definitions>>(
216 endpointName: EndpointName,
217 arg: QueryArgFrom<Definitions[EndpointName]>,
218 patches: readonly Patch[],
219 updateProvided?: boolean,
220) => ThunkAction<void, PartialState, any, UnknownAction>
221
222export type AllQueryKeys<Definitions extends EndpointDefinitions> =
223 | QueryKeys<Definitions>
224 | InfiniteQueryKeys<Definitions>
225
226export type QueryArgFromAnyQueryDefinition<
227 Definitions extends EndpointDefinitions,
228 EndpointName extends AllQueryKeys<Definitions>,
229> =
230 Definitions[EndpointName] extends InfiniteQueryDefinition<
231 any,
232 any,
233 any,
234 any,
235 any
236 >
237 ? InfiniteQueryArgFrom<Definitions[EndpointName]>
238 : Definitions[EndpointName] extends QueryDefinition<any, any, any, any>
239 ? QueryArgFrom<Definitions[EndpointName]>
240 : never
241
242export type DataFromAnyQueryDefinition<
243 Definitions extends EndpointDefinitions,
244 EndpointName extends AllQueryKeys<Definitions>,
245> =
246 Definitions[EndpointName] extends InfiniteQueryDefinition<
247 any,
248 any,
249 any,
250 any,
251 any
252 >
253 ? InfiniteData<
254 ResultTypeFrom<Definitions[EndpointName]>,
255 PageParamFrom<Definitions[EndpointName]>
256 >
257 : Definitions[EndpointName] extends QueryDefinition<any, any, any, any>
258 ? ResultTypeFrom<Definitions[EndpointName]>
259 : unknown
260
261export type UpsertThunkResult<
262 Definitions extends EndpointDefinitions,
263 EndpointName extends AllQueryKeys<Definitions>,
264> =
265 Definitions[EndpointName] extends InfiniteQueryDefinition<
266 any,
267 any,
268 any,
269 any,
270 any
271 >
272 ? InfiniteQueryActionCreatorResult<Definitions[EndpointName]>
273 : Definitions[EndpointName] extends QueryDefinition<any, any, any, any>
274 ? QueryActionCreatorResult<Definitions[EndpointName]>
275 : QueryActionCreatorResult<never>
276
277export type UpdateQueryDataThunk<
278 Definitions extends EndpointDefinitions,
279 PartialState,
280> = <EndpointName extends AllQueryKeys<Definitions>>(
281 endpointName: EndpointName,
282 arg: QueryArgFromAnyQueryDefinition<Definitions, EndpointName>,
283 updateRecipe: Recipe<DataFromAnyQueryDefinition<Definitions, EndpointName>>,
284 updateProvided?: boolean,
285) => ThunkAction<PatchCollection, PartialState, any, UnknownAction>
286
287export type UpsertQueryDataThunk<
288 Definitions extends EndpointDefinitions,
289 PartialState,
290> = <EndpointName extends AllQueryKeys<Definitions>>(
291 endpointName: EndpointName,
292 arg: QueryArgFromAnyQueryDefinition<Definitions, EndpointName>,
293 value: DataFromAnyQueryDefinition<Definitions, EndpointName>,
294) => ThunkAction<
295 UpsertThunkResult<Definitions, EndpointName>,
296 PartialState,
297 any,
298 UnknownAction
299>
300
301/**
302 * An object returned from dispatching a `api.util.updateQueryData` call.
303 */
304export type PatchCollection = {
305 /**
306 * An `immer` Patch describing the cache update.
307 */
308 patches: Patch[]
309 /**
310 * An `immer` Patch to revert the cache update.
311 */
312 inversePatches: Patch[]
313 /**
314 * A function that will undo the cache update.
315 */
316 undo: () => void
317}
318
319type TransformCallback = (
320 baseQueryReturnValue: unknown,
321 meta: unknown,
322 arg: unknown,
323) => any
324
325export const addShouldAutoBatch = <T extends Record<string, any>>(
326 arg: T = {} as T,
327): T & { [SHOULD_AUTOBATCH]: true } => {
328 return { ...arg, [SHOULD_AUTOBATCH]: true }
329}
330
331export function buildThunks<
332 BaseQuery extends BaseQueryFn,
333 ReducerPath extends string,
334 Definitions extends EndpointDefinitions,
335>({
336 reducerPath,
337 baseQuery,
338 context: { endpointDefinitions },
339 serializeQueryArgs,
340 api,
341 assertTagType,
342 selectors,
343 onSchemaFailure,
344 catchSchemaFailure: globalCatchSchemaFailure,
345 skipSchemaValidation: globalSkipSchemaValidation,
346}: {
347 baseQuery: BaseQuery
348 reducerPath: ReducerPath
349 context: ApiContext<Definitions>
350 serializeQueryArgs: InternalSerializeQueryArgs
351 api: Api<BaseQuery, Definitions, ReducerPath, any>
352 assertTagType: AssertTagTypes
353 selectors: AllSelectors
354 onSchemaFailure: SchemaFailureHandler | undefined
355 catchSchemaFailure: SchemaFailureConverter<BaseQuery> | undefined
356 skipSchemaValidation: boolean | SchemaType[] | undefined
357}) {
358 type State = RootState<any, string, ReducerPath>
359
360 const patchQueryData: PatchQueryDataThunk<EndpointDefinitions, State> =
361 (endpointName, arg, patches, updateProvided) => (dispatch, getState) => {
362 const endpointDefinition = endpointDefinitions[endpointName]
363
364 const queryCacheKey = serializeQueryArgs({
365 queryArgs: arg,
366 endpointDefinition,
367 endpointName,
368 })
369
370 dispatch(
371 api.internalActions.queryResultPatched({ queryCacheKey, patches }),
372 )
373
374 if (!updateProvided) {
375 return
376 }
377
378 const newValue = api.endpoints[endpointName].select(arg)(
379 // Work around TS 4.1 mismatch
380 getState() as RootState<any, any, any>,
381 )
382
383 const providedTags = calculateProvidedBy(
384 endpointDefinition.providesTags,
385 newValue.data,
386 undefined,
387 arg,
388 {},
389 assertTagType,
390 )
391
392 dispatch(
393 api.internalActions.updateProvidedBy([{ queryCacheKey, providedTags }]),
394 )
395 }
396
397 function addToStart<T>(items: Array<T>, item: T, max = 0): Array<T> {
398 const newItems = [item, ...items]
399 return max && newItems.length > max ? newItems.slice(0, -1) : newItems
400 }
401
402 function addToEnd<T>(items: Array<T>, item: T, max = 0): Array<T> {
403 const newItems = [...items, item]
404 return max && newItems.length > max ? newItems.slice(1) : newItems
405 }
406
407 const updateQueryData: UpdateQueryDataThunk<EndpointDefinitions, State> =
408 (endpointName, arg, updateRecipe, updateProvided = true) =>
409 (dispatch, getState) => {
410 const endpointDefinition = api.endpoints[endpointName]
411
412 const currentState = endpointDefinition.select(arg)(
413 // Work around TS 4.1 mismatch
414 getState() as RootState<any, any, any>,
415 )
416
417 const ret: PatchCollection = {
418 patches: [],
419 inversePatches: [],
420 undo: () =>
421 dispatch(
422 api.util.patchQueryData(
423 endpointName,
424 arg,
425 ret.inversePatches,
426 updateProvided,
427 ),
428 ),
429 }
430 if (currentState.status === STATUS_UNINITIALIZED) {
431 return ret
432 }
433 let newValue
434 if ('data' in currentState) {
435 if (isDraftable(currentState.data)) {
436 const [value, patches, inversePatches] = produceWithPatches(
437 currentState.data,
438 updateRecipe,
439 )
440 ret.patches.push(...patches)
441 ret.inversePatches.push(...inversePatches)
442 newValue = value
443 } else {
444 newValue = updateRecipe(currentState.data)
445 ret.patches.push({ op: 'replace', path: [], value: newValue })
446 ret.inversePatches.push({
447 op: 'replace',
448 path: [],
449 value: currentState.data,
450 })
451 }
452 }
453
454 if (ret.patches.length === 0) {
455 return ret
456 }
457
458 dispatch(
459 api.util.patchQueryData(endpointName, arg, ret.patches, updateProvided),
460 )
461
462 return ret
463 }
464
465 const upsertQueryData: UpsertQueryDataThunk<Definitions, State> =
466 (endpointName, arg, value) => (dispatch) => {
467 type EndpointName = typeof endpointName
468 const res = dispatch(
469 (
470 api.endpoints[endpointName] as ApiEndpointQuery<
471 QueryDefinition<any, any, any, any, any>,
472 Definitions
473 >
474 ).initiate(arg, {
475 subscribe: false,
476 forceRefetch: true,
477 [forceQueryFnSymbol]: () => ({ data: value }),
478 }),
479 ) as UpsertThunkResult<Definitions, EndpointName>
480
481 return res
482 }
483
484 const getTransformCallbackForEndpoint = (
485 endpointDefinition: EndpointDefinition<any, any, any, any>,
486 transformFieldName: 'transformResponse' | 'transformErrorResponse',
487 ): TransformCallback => {
488 return endpointDefinition.query && endpointDefinition[transformFieldName]
489 ? (endpointDefinition[transformFieldName]! as TransformCallback)
490 : defaultTransformResponse
491 }
492
493 // The generic async payload function for all of our thunks
494 const executeEndpoint: AsyncThunkPayloadCreator<
495 ThunkResult,
496 QueryThunkArg | MutationThunkArg | InfiniteQueryThunkArg<any>,
497 ThunkApiMetaConfig & { state: RootState<any, string, ReducerPath> }
498 > = async (
499 arg,
500 {
501 signal,
502 abort,
503 rejectWithValue,
504 fulfillWithValue,
505 dispatch,
506 getState,
507 extra,
508 },
509 ) => {
510 const endpointDefinition = endpointDefinitions[arg.endpointName]
511 const { metaSchema, skipSchemaValidation = globalSkipSchemaValidation } =
512 endpointDefinition
513
514 const isQuery = arg.type === ENDPOINT_QUERY
515
516 try {
517 let transformResponse: TransformCallback = defaultTransformResponse
518
519 const baseQueryApi = {
520 signal,
521 abort,
522 dispatch,
523 getState,
524 extra,
525 endpoint: arg.endpointName,
526 type: arg.type,
527 forced: isQuery ? isForcedQuery(arg, getState()) : undefined,
528 queryCacheKey: isQuery ? arg.queryCacheKey : undefined,
529 }
530
531 const forceQueryFn = isQuery ? arg[forceQueryFnSymbol] : undefined
532
533 let finalQueryReturnValue: QueryReturnValue
534
535 // Infinite query wrapper, which executes the request and returns
536 // the InfiniteData `{pages, pageParams}` structure
537 const fetchPage = async (
538 data: InfiniteData<unknown, unknown>,
539 param: unknown,
540 maxPages: number,
541 previous?: boolean,
542 ): Promise<QueryReturnValue> => {
543 // This should handle cases where there is no `getPrevPageParam`,
544 // or `getPPP` returned nullish
545 if (param == null && data.pages.length) {
546 return Promise.resolve({ data })
547 }
548
549 const finalQueryArg: InfiniteQueryCombinedArg<any, any> = {
550 queryArg: arg.originalArgs,
551 pageParam: param,
552 }
553
554 const pageResponse = await executeRequest(finalQueryArg)
555
556 const addTo = previous ? addToStart : addToEnd
557
558 return {
559 data: {
560 pages: addTo(data.pages, pageResponse.data, maxPages),
561 pageParams: addTo(data.pageParams, param, maxPages),
562 },
563 meta: pageResponse.meta,
564 }
565 }
566
567 // Wrapper for executing either `query` or `queryFn`,
568 // and handling any errors
569 async function executeRequest(
570 finalQueryArg: unknown,
571 ): Promise<QueryReturnValue> {
572 let result: QueryReturnValue
573 const { extraOptions, argSchema, rawResponseSchema, responseSchema } =
574 endpointDefinition
575
576 if (argSchema && !shouldSkip(skipSchemaValidation, 'arg')) {
577 finalQueryArg = await parseWithSchema(
578 argSchema,
579 finalQueryArg,
580 'argSchema',
581 {}, // we don't have a meta yet, so we can't pass it
582 )
583 }
584
585 if (forceQueryFn) {
586 // upsertQueryData relies on this to pass in the user-provided value
587 result = forceQueryFn()
588 } else if (endpointDefinition.query) {
589 // We should only run `transformResponse` when the endpoint has a `query` method,
590 // and we're not doing an `upsertQueryData`.
591 transformResponse = getTransformCallbackForEndpoint(
592 endpointDefinition,
593 'transformResponse',
594 )
595
596 result = await baseQuery(
597 endpointDefinition.query(finalQueryArg as any),
598 baseQueryApi,
599 extraOptions as any,
600 )
601 } else {
602 result = await endpointDefinition.queryFn(
603 finalQueryArg as any,
604 baseQueryApi,
605 extraOptions as any,
606 (arg) => baseQuery(arg, baseQueryApi, extraOptions as any),
607 )
608 }
609
610 if (
611 typeof process !== 'undefined' &&
612 process.env.NODE_ENV === 'development'
613 ) {
614 const what = endpointDefinition.query ? '`baseQuery`' : '`queryFn`'
615 let err: undefined | string
616 if (!result) {
617 err = `${what} did not return anything.`
618 } else if (typeof result !== 'object') {
619 err = `${what} did not return an object.`
620 } else if (result.error && result.data) {
621 err = `${what} returned an object containing both \`error\` and \`result\`.`
622 } else if (result.error === undefined && result.data === undefined) {
623 err = `${what} returned an object containing neither a valid \`error\` and \`result\`. At least one of them should not be \`undefined\``
624 } else {
625 for (const key of Object.keys(result)) {
626 if (key !== 'error' && key !== 'data' && key !== 'meta') {
627 err = `The object returned by ${what} has the unknown property ${key}.`
628 break
629 }
630 }
631 }
632 if (err) {
633 console.error(
634 `Error encountered handling the endpoint ${arg.endpointName}.
635 ${err}
636 It needs to return an object with either the shape \`{ data: <value> }\` or \`{ error: <value> }\` that may contain an optional \`meta\` property.
637 Object returned was:`,
638 result,
639 )
640 }
641 }
642
643 if (result.error) throw new HandledError(result.error, result.meta)
644
645 let { data } = result
646
647 if (
648 rawResponseSchema &&
649 !shouldSkip(skipSchemaValidation, 'rawResponse')
650 ) {
651 data = await parseWithSchema(
652 rawResponseSchema,
653 result.data,
654 'rawResponseSchema',
655 result.meta,
656 )
657 }
658
659 let transformedResponse = await transformResponse(
660 data,
661 result.meta,
662 finalQueryArg,
663 )
664
665 if (responseSchema && !shouldSkip(skipSchemaValidation, 'response')) {
666 transformedResponse = await parseWithSchema(
667 responseSchema,
668 transformedResponse,
669 'responseSchema',
670 result.meta,
671 )
672 }
673
674 return {
675 ...result,
676 data: transformedResponse,
677 }
678 }
679
680 if (isQuery && 'infiniteQueryOptions' in endpointDefinition) {
681 // This is an infinite query endpoint
682 const { infiniteQueryOptions } = endpointDefinition
683
684 // Runtime checks should guarantee this is a positive number if provided
685 const { maxPages = Infinity } = infiniteQueryOptions
686
687 // Priority: per-call override > endpoint config > default (true)
688 const refetchCachedPages =
689 (arg as InfiniteQueryThunkArg<any>).refetchCachedPages ??
690 infiniteQueryOptions.refetchCachedPages ??
691 true
692
693 let result: QueryReturnValue
694
695 // Start by looking up the existing InfiniteData value from state,
696 // falling back to an empty value if it doesn't exist yet
697 const blankData = { pages: [], pageParams: [] }
698 const cachedData = selectors.selectQueryEntry(
699 getState(),
700 arg.queryCacheKey,
701 )?.data as InfiniteData<unknown, unknown> | undefined
702
703 // When the arg changes or the user forces a refetch,
704 // we don't include the `direction` flag. This lets us distinguish
705 // between actually refetching with a forced query, vs just fetching
706 // the next page.
707 const isForcedQueryNeedingRefetch = // arg.forceRefetch
708 isForcedQuery(arg, getState()) &&
709 !(arg as InfiniteQueryThunkArg<any>).direction
710 const existingData = (
711 isForcedQueryNeedingRefetch || !cachedData ? blankData : cachedData
712 ) as InfiniteData<unknown, unknown>
713
714 // If the thunk specified a direction and we do have at least one page,
715 // fetch the next or previous page
716 if ('direction' in arg && arg.direction && existingData.pages.length) {
717 const previous = arg.direction === 'backward'
718 const pageParamFn = previous ? getPreviousPageParam : getNextPageParam
719 const param = pageParamFn(
720 infiniteQueryOptions,
721 existingData,
722 arg.originalArgs,
723 )
724
725 result = await fetchPage(existingData, param, maxPages, previous)
726 } else {
727 // Otherwise, fetch the first page and then any remaining pages
728
729 const { initialPageParam = infiniteQueryOptions.initialPageParam } =
730 arg as InfiniteQueryThunkArg<any>
731
732 // If we're doing a refetch, we should start from
733 // the first page we have cached.
734 // Otherwise, we should start from the initialPageParam
735 const cachedPageParams = cachedData?.pageParams ?? []
736 const firstPageParam = cachedPageParams[0] ?? initialPageParam
737 const totalPages = cachedPageParams.length
738
739 // Fetch first page
740 result = await fetchPage(existingData, firstPageParam, maxPages)
741
742 if (forceQueryFn) {
743 // HACK `upsertQueryData` expects the user to pass in the `{pages, pageParams}` structure,
744 // but `fetchPage` treats that as `pages[0]`. We have to manually un-nest it.
745 result = {
746 data: (result.data as InfiniteData<unknown, unknown>).pages[0],
747 } as QueryReturnValue
748 }
749
750 if (refetchCachedPages) {
751 // Fetch remaining pages
752 for (let i = 1; i < totalPages; i++) {
753 const param = getNextPageParam(
754 infiniteQueryOptions,
755 result.data as InfiniteData<unknown, unknown>,
756 arg.originalArgs,
757 )
758 result = await fetchPage(
759 result.data as InfiniteData<unknown, unknown>,
760 param,
761 maxPages,
762 )
763 }
764 }
765 }
766
767 finalQueryReturnValue = result
768 } else {
769 // Non-infinite endpoint. Just run the one request.
770 finalQueryReturnValue = await executeRequest(arg.originalArgs)
771 }
772
773 if (
774 metaSchema &&
775 !shouldSkip(skipSchemaValidation, 'meta') &&
776 finalQueryReturnValue.meta
777 ) {
778 finalQueryReturnValue.meta = await parseWithSchema(
779 metaSchema,
780 finalQueryReturnValue.meta,
781 'metaSchema',
782 finalQueryReturnValue.meta,
783 )
784 }
785
786 // console.log('Final result: ', transformedData)
787 return fulfillWithValue(
788 finalQueryReturnValue.data,
789 addShouldAutoBatch({
790 fulfilledTimeStamp: Date.now(),
791 baseQueryMeta: finalQueryReturnValue.meta,
792 }),
793 )
794 } catch (error) {
795 let caughtError = error
796 if (caughtError instanceof HandledError) {
797 let transformErrorResponse = getTransformCallbackForEndpoint(
798 endpointDefinition,
799 'transformErrorResponse',
800 )
801 const { rawErrorResponseSchema, errorResponseSchema } =
802 endpointDefinition
803
804 let { value, meta } = caughtError
805
806 try {
807 if (
808 rawErrorResponseSchema &&
809 !shouldSkip(skipSchemaValidation, 'rawErrorResponse')
810 ) {
811 value = await parseWithSchema(
812 rawErrorResponseSchema,
813 value,
814 'rawErrorResponseSchema',
815 meta,
816 )
817 }
818
819 if (metaSchema && !shouldSkip(skipSchemaValidation, 'meta')) {
820 meta = await parseWithSchema(metaSchema, meta, 'metaSchema', meta)
821 }
822 let transformedErrorResponse = await transformErrorResponse(
823 value,
824 meta,
825 arg.originalArgs,
826 )
827 if (
828 errorResponseSchema &&
829 !shouldSkip(skipSchemaValidation, 'errorResponse')
830 ) {
831 transformedErrorResponse = await parseWithSchema(
832 errorResponseSchema,
833 transformedErrorResponse,
834 'errorResponseSchema',
835 meta,
836 )
837 }
838
839 return rejectWithValue(
840 transformedErrorResponse,
841 addShouldAutoBatch({ baseQueryMeta: meta }),
842 )
843 } catch (e) {
844 caughtError = e
845 }
846 }
847 try {
848 if (caughtError instanceof NamedSchemaError) {
849 const info: SchemaFailureInfo = {
850 endpoint: arg.endpointName,
851 arg: arg.originalArgs,
852 type: arg.type,
853 queryCacheKey: isQuery ? arg.queryCacheKey : undefined,
854 }
855 endpointDefinition.onSchemaFailure?.(caughtError, info)
856 onSchemaFailure?.(caughtError, info)
857 const { catchSchemaFailure = globalCatchSchemaFailure } =
858 endpointDefinition
859 if (catchSchemaFailure) {
860 return rejectWithValue(
861 catchSchemaFailure(caughtError, info),
862 addShouldAutoBatch({ baseQueryMeta: caughtError._bqMeta }),
863 )
864 }
865 }
866 } catch (e) {
867 caughtError = e
868 }
869 if (
870 typeof process !== 'undefined' &&
871 process.env.NODE_ENV !== 'production'
872 ) {
873 console.error(
874 `An unhandled error occurred processing a request for the endpoint "${arg.endpointName}".
875In the case of an unhandled error, no tags will be "provided" or "invalidated".`,
876 caughtError,
877 )
878 } else {
879 console.error(caughtError)
880 }
881 throw caughtError
882 }
883 }
884
885 function isForcedQuery(
886 arg: QueryThunkArg,
887 state: RootState<any, string, ReducerPath>,
888 ) {
889 const requestState = selectors.selectQueryEntry(state, arg.queryCacheKey)
890 const baseFetchOnMountOrArgChange =
891 selectors.selectConfig(state).refetchOnMountOrArgChange
892
893 const fulfilledVal = requestState?.fulfilledTimeStamp
894 const refetchVal =
895 arg.forceRefetch ?? (arg.subscribe && baseFetchOnMountOrArgChange)
896
897 if (refetchVal) {
898 // Return if it's true or compare the dates because it must be a number
899 return (
900 refetchVal === true ||
901 (Number(new Date()) - Number(fulfilledVal)) / 1000 >= refetchVal
902 )
903 }
904 return false
905 }
906
907 const createQueryThunk = <
908 ThunkArgType extends QueryThunkArg | InfiniteQueryThunkArg<any>,
909 >() => {
910 const generatedQueryThunk = createAsyncThunk<
911 ThunkResult,
912 ThunkArgType,
913 ThunkApiMetaConfig & { state: RootState<any, string, ReducerPath> }
914 >(`${reducerPath}/executeQuery`, executeEndpoint, {
915 getPendingMeta({ arg }) {
916 const endpointDefinition = endpointDefinitions[arg.endpointName]
917 return addShouldAutoBatch({
918 startedTimeStamp: Date.now(),
919 ...(isInfiniteQueryDefinition(endpointDefinition)
920 ? { direction: (arg as InfiniteQueryThunkArg<any>).direction }
921 : {}),
922 })
923 },
924 condition(queryThunkArg, { getState }) {
925 const state = getState()
926
927 const requestState = selectors.selectQueryEntry(
928 state,
929 queryThunkArg.queryCacheKey,
930 )
931 const fulfilledVal = requestState?.fulfilledTimeStamp
932 const currentArg = queryThunkArg.originalArgs
933 const previousArg = requestState?.originalArgs
934 const endpointDefinition =
935 endpointDefinitions[queryThunkArg.endpointName]
936 const direction = (queryThunkArg as InfiniteQueryThunkArg<any>)
937 .direction
938
939 // Order of these checks matters.
940 // In order for `upsertQueryData` to successfully run while an existing request is in flight,
941 /// we have to check for that first, otherwise `queryThunk` will bail out and not run at all.
942 if (isUpsertQuery(queryThunkArg)) {
943 return true
944 }
945
946 // Don't retry a request that's currently in-flight
947 if (requestState?.status === 'pending') {
948 return false
949 }
950
951 // if this is forced, continue
952 if (isForcedQuery(queryThunkArg, state)) {
953 return true
954 }
955
956 if (
957 isQueryDefinition(endpointDefinition) &&
958 endpointDefinition?.forceRefetch?.({
959 currentArg,
960 previousArg,
961 endpointState: requestState,
962 state,
963 })
964 ) {
965 return true
966 }
967
968 // Pull from the cache unless we explicitly force refetch or qualify based on time
969 if (fulfilledVal && !direction) {
970 // Value is cached and we didn't specify to refresh, skip it.
971 return false
972 }
973
974 return true
975 },
976 dispatchConditionRejection: true,
977 })
978 return generatedQueryThunk
979 }
980
981 const queryThunk = createQueryThunk<QueryThunkArg>()
982 const infiniteQueryThunk = createQueryThunk<InfiniteQueryThunkArg<any>>()
983
984 const mutationThunk = createAsyncThunk<
985 ThunkResult,
986 MutationThunkArg,
987 ThunkApiMetaConfig & { state: RootState<any, string, ReducerPath> }
988 >(`${reducerPath}/executeMutation`, executeEndpoint, {
989 getPendingMeta() {
990 return addShouldAutoBatch({ startedTimeStamp: Date.now() })
991 },
992 })
993
994 const hasTheForce = (options: any): options is { force: boolean } =>
995 'force' in options
996 const hasMaxAge = (
997 options: any,
998 ): options is { ifOlderThan: false | number } => 'ifOlderThan' in options
999
1000 const prefetch =
1001 <EndpointName extends QueryKeys<Definitions>>(
1002 endpointName: EndpointName,
1003 arg: any,
1004 options: PrefetchOptions = {},
1005 ): ThunkAction<void, any, any, UnknownAction> =>
1006 (dispatch: ThunkDispatch<any, any, any>, getState: () => any) => {
1007 const force = hasTheForce(options) && options.force
1008 const maxAge = hasMaxAge(options) && options.ifOlderThan
1009
1010 const queryAction = (force: boolean = true) => {
1011 const options: StartQueryActionCreatorOptions = {
1012 forceRefetch: force,
1013 subscribe: false,
1014 }
1015 return (
1016 api.endpoints[endpointName] as ApiEndpointQuery<any, any>
1017 ).initiate(arg, options)
1018 }
1019 const latestStateValue = (
1020 api.endpoints[endpointName] as ApiEndpointQuery<any, any>
1021 ).select(arg)(getState())
1022
1023 if (force) {
1024 dispatch(queryAction())
1025 } else if (maxAge) {
1026 const lastFulfilledTs = latestStateValue?.fulfilledTimeStamp
1027 if (!lastFulfilledTs) {
1028 dispatch(queryAction())
1029 return
1030 }
1031 const shouldRetrigger =
1032 (Number(new Date()) - Number(new Date(lastFulfilledTs))) / 1000 >=
1033 maxAge
1034 if (shouldRetrigger) {
1035 dispatch(queryAction())
1036 }
1037 } else {
1038 // If prefetching with no options, just let it try
1039 dispatch(queryAction(false))
1040 }
1041 }
1042
1043 function matchesEndpoint(endpointName: string) {
1044 return (action: any): action is UnknownAction =>
1045 action?.meta?.arg?.endpointName === endpointName
1046 }
1047
1048 function buildMatchThunkActions<
1049 Thunk extends
1050 | AsyncThunk<any, QueryThunkArg, ThunkApiMetaConfig>
1051 | AsyncThunk<any, MutationThunkArg, ThunkApiMetaConfig>,
1052 >(thunk: Thunk, endpointName: string) {
1053 return {
1054 matchPending: isAllOf(isPending(thunk), matchesEndpoint(endpointName)),
1055 matchFulfilled: isAllOf(
1056 isFulfilled(thunk),
1057 matchesEndpoint(endpointName),
1058 ),
1059 matchRejected: isAllOf(isRejected(thunk), matchesEndpoint(endpointName)),
1060 } as Matchers<Thunk, any>
1061 }
1062
1063 return {
1064 queryThunk,
1065 mutationThunk,
1066 infiniteQueryThunk,
1067 prefetch,
1068 updateQueryData,
1069 upsertQueryData,
1070 patchQueryData,
1071 buildMatchThunkActions,
1072 }
1073}
1074
1075export function getNextPageParam(
1076 options: InfiniteQueryConfigOptions<unknown, unknown, unknown>,
1077 { pages, pageParams }: InfiniteData<unknown, unknown>,
1078 queryArg: unknown,
1079): unknown | undefined {
1080 const lastIndex = pages.length - 1
1081 return options.getNextPageParam(
1082 pages[lastIndex],
1083 pages,
1084 pageParams[lastIndex],
1085 pageParams,
1086 queryArg,
1087 )
1088}
1089
1090export function getPreviousPageParam(
1091 options: InfiniteQueryConfigOptions<unknown, unknown, unknown>,
1092 { pages, pageParams }: InfiniteData<unknown, unknown>,
1093 queryArg: unknown,
1094): unknown | undefined {
1095 return options.getPreviousPageParam?.(
1096 pages[0],
1097 pages,
1098 pageParams[0],
1099 pageParams,
1100 queryArg,
1101 )
1102}
1103
1104export function calculateProvidedByThunk(
1105 action: UnwrapPromise<
1106 | ReturnType<ReturnType<QueryThunk>>
1107 | ReturnType<ReturnType<MutationThunk>>
1108 | ReturnType<ReturnType<InfiniteQueryThunk<any>>>
1109 >,
1110 type: 'providesTags' | 'invalidatesTags',
1111 endpointDefinitions: EndpointDefinitions,
1112 assertTagType: AssertTagTypes,
1113) {
1114 return calculateProvidedBy(
1115 endpointDefinitions[action.meta.arg.endpointName][
1116 type
1117 ] as ResultDescription<any, any, any, any, any>,
1118 isFulfilled(action) ? action.payload : undefined,
1119 isRejectedWithValue(action) ? action.payload : undefined,
1120 action.meta.arg.originalArgs,
1121 'baseQueryMeta' in action.meta ? action.meta.baseQueryMeta : undefined,
1122 assertTagType,
1123 )
1124}
Note: See TracBrowser for help on using the repository browser.