@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
1,084 lines (976 loc) • 34.1 kB
text/typescript
import type {
AsyncThunk,
AsyncThunkPayloadCreator,
Draft,
ThunkAction,
ThunkDispatch,
UnknownAction,
} from '@reduxjs/toolkit'
import type { Patch } from 'immer'
import { isDraftable, produceWithPatches } from 'immer'
import type { Api, ApiContext } from '../apiTypes'
import type {
BaseQueryError,
BaseQueryFn,
QueryReturnValue,
} from '../baseQueryTypes'
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
import type {
AssertTagTypes,
EndpointDefinition,
EndpointDefinitions,
InfiniteQueryArgFrom,
InfiniteQueryCombinedArg,
InfiniteQueryDefinition,
MutationDefinition,
PageParamFrom,
QueryArgFrom,
QueryDefinition,
ResultDescription,
ResultTypeFrom,
SchemaFailureConverter,
SchemaFailureHandler,
SchemaFailureInfo,
} from '../endpointDefinitions'
import {
calculateProvidedBy,
isInfiniteQueryDefinition,
isQueryDefinition,
} from '../endpointDefinitions'
import { HandledError } from '../HandledError'
import type { UnwrapPromise } from '../tsHelpers'
import type {
RootState,
QueryKeys,
QuerySubstateIdentifier,
InfiniteData,
InfiniteQueryConfigOptions,
QueryCacheKey,
InfiniteQueryDirection,
InfiniteQueryKeys,
} from './apiState'
import { QueryStatus } from './apiState'
import type {
InfiniteQueryActionCreatorResult,
QueryActionCreatorResult,
StartInfiniteQueryActionCreatorOptions,
StartQueryActionCreatorOptions,
} from './buildInitiate'
import { forceQueryFnSymbol, isUpsertQuery } from './buildInitiate'
import type { AllSelectors } from './buildSelectors'
import type { ApiEndpointQuery, PrefetchOptions } from './module'
import {
createAsyncThunk,
isAllOf,
isFulfilled,
isPending,
isRejected,
isRejectedWithValue,
SHOULD_AUTOBATCH,
} from './rtkImports'
import { parseWithSchema, NamedSchemaError } from '../standardSchema'
export type BuildThunksApiEndpointQuery<
Definition extends QueryDefinition<any, any, any, any, any>,
> = Matchers<QueryThunk, Definition>
export type BuildThunksApiEndpointInfiniteQuery<
Definition extends InfiniteQueryDefinition<any, any, any, any, any>,
> = Matchers<InfiniteQueryThunk<any>, Definition>
export type BuildThunksApiEndpointMutation<
Definition extends MutationDefinition<any, any, any, any, any>,
> = Matchers<MutationThunk, Definition>
type EndpointThunk<
Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
Definition extends EndpointDefinition<any, any, any, any>,
> =
Definition extends EndpointDefinition<
infer QueryArg,
infer BaseQueryFn,
any,
infer ResultType
>
? Thunk extends AsyncThunk<unknown, infer ATArg, infer ATConfig>
? AsyncThunk<
ResultType,
ATArg & { originalArgs: QueryArg },
ATConfig & { rejectValue: BaseQueryError<BaseQueryFn> }
>
: never
: Definition extends InfiniteQueryDefinition<
infer QueryArg,
infer PageParam,
infer BaseQueryFn,
any,
infer ResultType
>
? Thunk extends AsyncThunk<unknown, infer ATArg, infer ATConfig>
? AsyncThunk<
InfiniteData<ResultType, PageParam>,
ATArg & { originalArgs: QueryArg },
ATConfig & { rejectValue: BaseQueryError<BaseQueryFn> }
>
: never
: never
export type PendingAction<
Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
Definition extends EndpointDefinition<any, any, any, any>,
> = ReturnType<EndpointThunk<Thunk, Definition>['pending']>
export type FulfilledAction<
Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
Definition extends EndpointDefinition<any, any, any, any>,
> = ReturnType<EndpointThunk<Thunk, Definition>['fulfilled']>
export type RejectedAction<
Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
Definition extends EndpointDefinition<any, any, any, any>,
> = ReturnType<EndpointThunk<Thunk, Definition>['rejected']>
export type Matcher<M> = (value: any) => value is M
export interface Matchers<
Thunk extends QueryThunk | MutationThunk | InfiniteQueryThunk<any>,
Definition extends EndpointDefinition<any, any, any, any>,
> {
matchPending: Matcher<PendingAction<Thunk, Definition>>
matchFulfilled: Matcher<FulfilledAction<Thunk, Definition>>
matchRejected: Matcher<RejectedAction<Thunk, Definition>>
}
export type QueryThunkArg = QuerySubstateIdentifier &
StartQueryActionCreatorOptions & {
type: 'query'
originalArgs: unknown
endpointName: string
}
export type InfiniteQueryThunkArg<
D extends InfiniteQueryDefinition<any, any, any, any, any>,
> = QuerySubstateIdentifier &
StartInfiniteQueryActionCreatorOptions<D> & {
type: `query`
originalArgs: unknown
endpointName: string
param: unknown
direction?: InfiniteQueryDirection
}
type MutationThunkArg = {
type: 'mutation'
originalArgs: unknown
endpointName: string
track?: boolean
fixedCacheKey?: string
}
export type ThunkResult = unknown
export type ThunkApiMetaConfig = {
pendingMeta: { startedTimeStamp: number; [SHOULD_AUTOBATCH]: true }
fulfilledMeta: {
fulfilledTimeStamp: number
baseQueryMeta: unknown
[SHOULD_AUTOBATCH]: true
}
rejectedMeta: { baseQueryMeta: unknown; [SHOULD_AUTOBATCH]: true }
}
export type QueryThunk = AsyncThunk<
ThunkResult,
QueryThunkArg,
ThunkApiMetaConfig
>
export type InfiniteQueryThunk<
D extends InfiniteQueryDefinition<any, any, any, any, any>,
> = AsyncThunk<ThunkResult, InfiniteQueryThunkArg<D>, ThunkApiMetaConfig>
export type MutationThunk = AsyncThunk<
ThunkResult,
MutationThunkArg,
ThunkApiMetaConfig
>
function defaultTransformResponse(baseQueryReturnValue: unknown) {
return baseQueryReturnValue
}
export type MaybeDrafted<T> = T | Draft<T>
export type Recipe<T> = (data: MaybeDrafted<T>) => void | MaybeDrafted<T>
export type UpsertRecipe<T> = (
data: MaybeDrafted<T> | undefined,
) => void | MaybeDrafted<T>
export type PatchQueryDataThunk<
Definitions extends EndpointDefinitions,
PartialState,
> = <EndpointName extends QueryKeys<Definitions>>(
endpointName: EndpointName,
arg: QueryArgFrom<Definitions[EndpointName]>,
patches: readonly Patch[],
updateProvided?: boolean,
) => ThunkAction<void, PartialState, any, UnknownAction>
export type AllQueryKeys<Definitions extends EndpointDefinitions> =
| QueryKeys<Definitions>
| InfiniteQueryKeys<Definitions>
export type QueryArgFromAnyQueryDefinition<
Definitions extends EndpointDefinitions,
EndpointName extends AllQueryKeys<Definitions>,
> =
Definitions[EndpointName] extends InfiniteQueryDefinition<
any,
any,
any,
any,
any
>
? InfiniteQueryArgFrom<Definitions[EndpointName]>
: Definitions[EndpointName] extends QueryDefinition<any, any, any, any>
? QueryArgFrom<Definitions[EndpointName]>
: never
export type DataFromAnyQueryDefinition<
Definitions extends EndpointDefinitions,
EndpointName extends AllQueryKeys<Definitions>,
> =
Definitions[EndpointName] extends InfiniteQueryDefinition<
any,
any,
any,
any,
any
>
? InfiniteData<
ResultTypeFrom<Definitions[EndpointName]>,
PageParamFrom<Definitions[EndpointName]>
>
: Definitions[EndpointName] extends QueryDefinition<any, any, any, any>
? ResultTypeFrom<Definitions[EndpointName]>
: unknown
export type UpsertThunkResult<
Definitions extends EndpointDefinitions,
EndpointName extends AllQueryKeys<Definitions>,
> =
Definitions[EndpointName] extends InfiniteQueryDefinition<
any,
any,
any,
any,
any
>
? InfiniteQueryActionCreatorResult<Definitions[EndpointName]>
: Definitions[EndpointName] extends QueryDefinition<any, any, any, any>
? QueryActionCreatorResult<Definitions[EndpointName]>
: QueryActionCreatorResult<never>
export type UpdateQueryDataThunk<
Definitions extends EndpointDefinitions,
PartialState,
> = <EndpointName extends AllQueryKeys<Definitions>>(
endpointName: EndpointName,
arg: QueryArgFromAnyQueryDefinition<Definitions, EndpointName>,
updateRecipe: Recipe<DataFromAnyQueryDefinition<Definitions, EndpointName>>,
updateProvided?: boolean,
) => ThunkAction<PatchCollection, PartialState, any, UnknownAction>
export type UpsertQueryDataThunk<
Definitions extends EndpointDefinitions,
PartialState,
> = <EndpointName extends AllQueryKeys<Definitions>>(
endpointName: EndpointName,
arg: QueryArgFromAnyQueryDefinition<Definitions, EndpointName>,
value: DataFromAnyQueryDefinition<Definitions, EndpointName>,
) => ThunkAction<
UpsertThunkResult<Definitions, EndpointName>,
PartialState,
any,
UnknownAction
>
/**
* An object returned from dispatching a `api.util.updateQueryData` call.
*/
export type PatchCollection = {
/**
* An `immer` Patch describing the cache update.
*/
patches: Patch[]
/**
* An `immer` Patch to revert the cache update.
*/
inversePatches: Patch[]
/**
* A function that will undo the cache update.
*/
undo: () => void
}
type TransformCallback = (
baseQueryReturnValue: unknown,
meta: unknown,
arg: unknown,
) => any
export const addShouldAutoBatch = <T extends Record<string, any>>(
arg: T = {} as T,
): T & { [SHOULD_AUTOBATCH]: true } => {
return { ...arg, [SHOULD_AUTOBATCH]: true }
}
export function buildThunks<
BaseQuery extends BaseQueryFn,
ReducerPath extends string,
Definitions extends EndpointDefinitions,
>({
reducerPath,
baseQuery,
context: { endpointDefinitions },
serializeQueryArgs,
api,
assertTagType,
selectors,
onSchemaFailure,
catchSchemaFailure: globalCatchSchemaFailure,
skipSchemaValidation: globalSkipSchemaValidation,
}: {
baseQuery: BaseQuery
reducerPath: ReducerPath
context: ApiContext<Definitions>
serializeQueryArgs: InternalSerializeQueryArgs
api: Api<BaseQuery, Definitions, ReducerPath, any>
assertTagType: AssertTagTypes
selectors: AllSelectors
onSchemaFailure: SchemaFailureHandler | undefined
catchSchemaFailure: SchemaFailureConverter<BaseQuery> | undefined
skipSchemaValidation: boolean | undefined
}) {
type State = RootState<any, string, ReducerPath>
const patchQueryData: PatchQueryDataThunk<EndpointDefinitions, State> =
(endpointName, arg, patches, updateProvided) => (dispatch, getState) => {
const endpointDefinition = endpointDefinitions[endpointName]
const queryCacheKey = serializeQueryArgs({
queryArgs: arg,
endpointDefinition,
endpointName,
})
dispatch(
api.internalActions.queryResultPatched({ queryCacheKey, patches }),
)
if (!updateProvided) {
return
}
const newValue = api.endpoints[endpointName].select(arg)(
// Work around TS 4.1 mismatch
getState() as RootState<any, any, any>,
)
const providedTags = calculateProvidedBy(
endpointDefinition.providesTags,
newValue.data,
undefined,
arg,
{},
assertTagType,
)
dispatch(
api.internalActions.updateProvidedBy([{ queryCacheKey, providedTags }]),
)
}
function addToStart<T>(items: Array<T>, item: T, max = 0): Array<T> {
const newItems = [item, ...items]
return max && newItems.length > max ? newItems.slice(0, -1) : newItems
}
function addToEnd<T>(items: Array<T>, item: T, max = 0): Array<T> {
const newItems = [...items, item]
return max && newItems.length > max ? newItems.slice(1) : newItems
}
const updateQueryData: UpdateQueryDataThunk<EndpointDefinitions, State> =
(endpointName, arg, updateRecipe, updateProvided = true) =>
(dispatch, getState) => {
const endpointDefinition = api.endpoints[endpointName]
const currentState = endpointDefinition.select(arg)(
// Work around TS 4.1 mismatch
getState() as RootState<any, any, any>,
)
const ret: PatchCollection = {
patches: [],
inversePatches: [],
undo: () =>
dispatch(
api.util.patchQueryData(
endpointName,
arg,
ret.inversePatches,
updateProvided,
),
),
}
if (currentState.status === QueryStatus.uninitialized) {
return ret
}
let newValue
if ('data' in currentState) {
if (isDraftable(currentState.data)) {
const [value, patches, inversePatches] = produceWithPatches(
currentState.data,
updateRecipe,
)
ret.patches.push(...patches)
ret.inversePatches.push(...inversePatches)
newValue = value
} else {
newValue = updateRecipe(currentState.data)
ret.patches.push({ op: 'replace', path: [], value: newValue })
ret.inversePatches.push({
op: 'replace',
path: [],
value: currentState.data,
})
}
}
if (ret.patches.length === 0) {
return ret
}
dispatch(
api.util.patchQueryData(endpointName, arg, ret.patches, updateProvided),
)
return ret
}
const upsertQueryData: UpsertQueryDataThunk<Definitions, State> =
(endpointName, arg, value) => (dispatch) => {
type EndpointName = typeof endpointName
const res = dispatch(
(
api.endpoints[endpointName] as ApiEndpointQuery<
QueryDefinition<any, any, any, any, any>,
Definitions
>
).initiate(arg, {
subscribe: false,
forceRefetch: true,
[forceQueryFnSymbol]: () => ({ data: value }),
}),
) as UpsertThunkResult<Definitions, EndpointName>
return res
}
const getTransformCallbackForEndpoint = (
endpointDefinition: EndpointDefinition<any, any, any, any>,
transformFieldName: 'transformResponse' | 'transformErrorResponse',
): TransformCallback => {
return endpointDefinition.query && endpointDefinition[transformFieldName]
? (endpointDefinition[transformFieldName]! as TransformCallback)
: defaultTransformResponse
}
// The generic async payload function for all of our thunks
const executeEndpoint: AsyncThunkPayloadCreator<
ThunkResult,
QueryThunkArg | MutationThunkArg | InfiniteQueryThunkArg<any>,
ThunkApiMetaConfig & { state: RootState<any, string, ReducerPath> }
> = async (
arg,
{
signal,
abort,
rejectWithValue,
fulfillWithValue,
dispatch,
getState,
extra,
},
) => {
const endpointDefinition = endpointDefinitions[arg.endpointName]
const { metaSchema, skipSchemaValidation = globalSkipSchemaValidation } =
endpointDefinition
try {
let transformResponse = getTransformCallbackForEndpoint(
endpointDefinition,
'transformResponse',
)
const baseQueryApi = {
signal,
abort,
dispatch,
getState,
extra,
endpoint: arg.endpointName,
type: arg.type,
forced:
arg.type === 'query' ? isForcedQuery(arg, getState()) : undefined,
queryCacheKey: arg.type === 'query' ? arg.queryCacheKey : undefined,
}
const forceQueryFn =
arg.type === 'query' ? arg[forceQueryFnSymbol] : undefined
let finalQueryReturnValue: QueryReturnValue
// Infinite query wrapper, which executes the request and returns
// the InfiniteData `{pages, pageParams}` structure
const fetchPage = async (
data: InfiniteData<unknown, unknown>,
param: unknown,
maxPages: number,
previous?: boolean,
): Promise<QueryReturnValue> => {
// This should handle cases where there is no `getPrevPageParam`,
// or `getPPP` returned nullish
if (param == null && data.pages.length) {
return Promise.resolve({ data })
}
const finalQueryArg: InfiniteQueryCombinedArg<any, any> = {
queryArg: arg.originalArgs,
pageParam: param,
}
const pageResponse = await executeRequest(finalQueryArg)
const addTo = previous ? addToStart : addToEnd
return {
data: {
pages: addTo(data.pages, pageResponse.data, maxPages),
pageParams: addTo(data.pageParams, param, maxPages),
},
meta: pageResponse.meta,
}
}
// Wrapper for executing either `query` or `queryFn`,
// and handling any errors
async function executeRequest(
finalQueryArg: unknown,
): Promise<QueryReturnValue> {
let result: QueryReturnValue
const { extraOptions, argSchema, rawResponseSchema, responseSchema } =
endpointDefinition
if (argSchema && !skipSchemaValidation) {
finalQueryArg = await parseWithSchema(
argSchema,
finalQueryArg,
'argSchema',
{}, // we don't have a meta yet, so we can't pass it
)
}
if (forceQueryFn) {
// upsertQueryData relies on this to pass in the user-provided value
result = forceQueryFn()
} else if (endpointDefinition.query) {
result = await baseQuery(
endpointDefinition.query(finalQueryArg as any),
baseQueryApi,
extraOptions as any,
)
} else {
result = await endpointDefinition.queryFn(
finalQueryArg as any,
baseQueryApi,
extraOptions as any,
(arg) => baseQuery(arg, baseQueryApi, extraOptions as any),
)
}
if (
typeof process !== 'undefined' &&
process.env.NODE_ENV === 'development'
) {
const what = endpointDefinition.query ? '`baseQuery`' : '`queryFn`'
let err: undefined | string
if (!result) {
err = `${what} did not return anything.`
} else if (typeof result !== 'object') {
err = `${what} did not return an object.`
} else if (result.error && result.data) {
err = `${what} returned an object containing both \`error\` and \`result\`.`
} else if (result.error === undefined && result.data === undefined) {
err = `${what} returned an object containing neither a valid \`error\` and \`result\`. At least one of them should not be \`undefined\``
} else {
for (const key of Object.keys(result)) {
if (key !== 'error' && key !== 'data' && key !== 'meta') {
err = `The object returned by ${what} has the unknown property ${key}.`
break
}
}
}
if (err) {
console.error(
`Error encountered handling the endpoint ${arg.endpointName}.
${err}
It needs to return an object with either the shape \`{ data: <value> }\` or \`{ error: <value> }\` that may contain an optional \`meta\` property.
Object returned was:`,
result,
)
}
}
if (result.error) throw new HandledError(result.error, result.meta)
let { data } = result
if (rawResponseSchema && !skipSchemaValidation) {
data = await parseWithSchema(
rawResponseSchema,
result.data,
'rawResponseSchema',
result.meta,
)
}
let transformedResponse = await transformResponse(
data,
result.meta,
finalQueryArg,
)
if (responseSchema && !skipSchemaValidation) {
transformedResponse = await parseWithSchema(
responseSchema,
transformedResponse,
'responseSchema',
result.meta,
)
}
return {
...result,
data: transformedResponse,
}
}
if (
arg.type === 'query' &&
'infiniteQueryOptions' in endpointDefinition
) {
// This is an infinite query endpoint
const { infiniteQueryOptions } = endpointDefinition
// Runtime checks should guarantee this is a positive number if provided
const { maxPages = Infinity } = infiniteQueryOptions
let result: QueryReturnValue
// Start by looking up the existing InfiniteData value from state,
// falling back to an empty value if it doesn't exist yet
const blankData = { pages: [], pageParams: [] }
const cachedData = selectors.selectQueryEntry(
getState(),
arg.queryCacheKey,
)?.data as InfiniteData<unknown, unknown> | undefined
// When the arg changes or the user forces a refetch,
// we don't include the `direction` flag. This lets us distinguish
// between actually refetching with a forced query, vs just fetching
// the next page.
const isForcedQueryNeedingRefetch = // arg.forceRefetch
isForcedQuery(arg, getState()) &&
!(arg as InfiniteQueryThunkArg<any>).direction
const existingData = (
isForcedQueryNeedingRefetch || !cachedData ? blankData : cachedData
) as InfiniteData<unknown, unknown>
// If the thunk specified a direction and we do have at least one page,
// fetch the next or previous page
if ('direction' in arg && arg.direction && existingData.pages.length) {
const previous = arg.direction === 'backward'
const pageParamFn = previous ? getPreviousPageParam : getNextPageParam
const param = pageParamFn(infiniteQueryOptions, existingData)
result = await fetchPage(existingData, param, maxPages, previous)
} else {
// Otherwise, fetch the first page and then any remaining pages
const { initialPageParam = infiniteQueryOptions.initialPageParam } =
arg as InfiniteQueryThunkArg<any>
// If we're doing a refetch, we should start from
// the first page we have cached.
// Otherwise, we should start from the initialPageParam
const cachedPageParams = cachedData?.pageParams ?? []
const firstPageParam = cachedPageParams[0] ?? initialPageParam
const totalPages = cachedPageParams.length
// Fetch first page
result = await fetchPage(existingData, firstPageParam, maxPages)
if (forceQueryFn) {
// HACK `upsertQueryData` expects the user to pass in the `{pages, pageParams}` structure,
// but `fetchPage` treats that as `pages[0]`. We have to manually un-nest it.
result = {
data: (result.data as InfiniteData<unknown, unknown>).pages[0],
} as QueryReturnValue
}
// Fetch remaining pages
for (let i = 1; i < totalPages; i++) {
const param = getNextPageParam(
infiniteQueryOptions,
result.data as InfiniteData<unknown, unknown>,
)
result = await fetchPage(
result.data as InfiniteData<unknown, unknown>,
param,
maxPages,
)
}
}
finalQueryReturnValue = result
} else {
// Non-infinite endpoint. Just run the one request.
finalQueryReturnValue = await executeRequest(arg.originalArgs)
}
if (metaSchema && !skipSchemaValidation && finalQueryReturnValue.meta) {
finalQueryReturnValue.meta = await parseWithSchema(
metaSchema,
finalQueryReturnValue.meta,
'metaSchema',
finalQueryReturnValue.meta,
)
}
// console.log('Final result: ', transformedData)
return fulfillWithValue(
finalQueryReturnValue.data,
addShouldAutoBatch({
fulfilledTimeStamp: Date.now(),
baseQueryMeta: finalQueryReturnValue.meta,
}),
)
} catch (error) {
let caughtError = error
if (caughtError instanceof HandledError) {
let transformErrorResponse = getTransformCallbackForEndpoint(
endpointDefinition,
'transformErrorResponse',
)
const { rawErrorResponseSchema, errorResponseSchema } =
endpointDefinition
let { value, meta } = caughtError
try {
if (rawErrorResponseSchema && !skipSchemaValidation) {
value = await parseWithSchema(
rawErrorResponseSchema,
value,
'rawErrorResponseSchema',
meta,
)
}
if (metaSchema && !skipSchemaValidation) {
meta = await parseWithSchema(metaSchema, meta, 'metaSchema', meta)
}
let transformedErrorResponse = await transformErrorResponse(
value,
meta,
arg.originalArgs,
)
if (errorResponseSchema && !skipSchemaValidation) {
transformedErrorResponse = await parseWithSchema(
errorResponseSchema,
transformedErrorResponse,
'errorResponseSchema',
meta,
)
}
return rejectWithValue(
transformedErrorResponse,
addShouldAutoBatch({ baseQueryMeta: meta }),
)
} catch (e) {
caughtError = e
}
}
try {
if (caughtError instanceof NamedSchemaError) {
const info: SchemaFailureInfo = {
endpoint: arg.endpointName,
arg: arg.originalArgs,
type: arg.type,
queryCacheKey: arg.type === 'query' ? arg.queryCacheKey : undefined,
}
endpointDefinition.onSchemaFailure?.(caughtError, info)
onSchemaFailure?.(caughtError, info)
const { catchSchemaFailure = globalCatchSchemaFailure } =
endpointDefinition
if (catchSchemaFailure) {
return rejectWithValue(
catchSchemaFailure(caughtError, info),
addShouldAutoBatch({ baseQueryMeta: caughtError._bqMeta }),
)
}
}
} catch (e) {
caughtError = e
}
if (
typeof process !== 'undefined' &&
process.env.NODE_ENV !== 'production'
) {
console.error(
`An unhandled error occurred processing a request for the endpoint "${arg.endpointName}".
In the case of an unhandled error, no tags will be "provided" or "invalidated".`,
caughtError,
)
} else {
console.error(caughtError)
}
throw caughtError
}
}
function isForcedQuery(
arg: QueryThunkArg,
state: RootState<any, string, ReducerPath>,
) {
const requestState = selectors.selectQueryEntry(state, arg.queryCacheKey)
const baseFetchOnMountOrArgChange =
selectors.selectConfig(state).refetchOnMountOrArgChange
const fulfilledVal = requestState?.fulfilledTimeStamp
const refetchVal =
arg.forceRefetch ?? (arg.subscribe && baseFetchOnMountOrArgChange)
if (refetchVal) {
// Return if it's true or compare the dates because it must be a number
return (
refetchVal === true ||
(Number(new Date()) - Number(fulfilledVal)) / 1000 >= refetchVal
)
}
return false
}
const createQueryThunk = <
ThunkArgType extends QueryThunkArg | InfiniteQueryThunkArg<any>,
>() => {
const generatedQueryThunk = createAsyncThunk<
ThunkResult,
ThunkArgType,
ThunkApiMetaConfig & { state: RootState<any, string, ReducerPath> }
>(`${reducerPath}/executeQuery`, executeEndpoint, {
getPendingMeta({ arg }) {
const endpointDefinition = endpointDefinitions[arg.endpointName]
return addShouldAutoBatch({
startedTimeStamp: Date.now(),
...(isInfiniteQueryDefinition(endpointDefinition)
? { direction: (arg as InfiniteQueryThunkArg<any>).direction }
: {}),
})
},
condition(queryThunkArg, { getState }) {
const state = getState()
const requestState = selectors.selectQueryEntry(
state,
queryThunkArg.queryCacheKey,
)
const fulfilledVal = requestState?.fulfilledTimeStamp
const currentArg = queryThunkArg.originalArgs
const previousArg = requestState?.originalArgs
const endpointDefinition =
endpointDefinitions[queryThunkArg.endpointName]
const direction = (queryThunkArg as InfiniteQueryThunkArg<any>)
.direction
// Order of these checks matters.
// In order for `upsertQueryData` to successfully run while an existing request is in flight,
/// we have to check for that first, otherwise `queryThunk` will bail out and not run at all.
if (isUpsertQuery(queryThunkArg)) {
return true
}
// Don't retry a request that's currently in-flight
if (requestState?.status === 'pending') {
return false
}
// if this is forced, continue
if (isForcedQuery(queryThunkArg, state)) {
return true
}
if (
isQueryDefinition(endpointDefinition) &&
endpointDefinition?.forceRefetch?.({
currentArg,
previousArg,
endpointState: requestState,
state,
})
) {
return true
}
// Pull from the cache unless we explicitly force refetch or qualify based on time
if (fulfilledVal && !direction) {
// Value is cached and we didn't specify to refresh, skip it.
return false
}
return true
},
dispatchConditionRejection: true,
})
return generatedQueryThunk
}
const queryThunk = createQueryThunk<QueryThunkArg>()
const infiniteQueryThunk = createQueryThunk<InfiniteQueryThunkArg<any>>()
const mutationThunk = createAsyncThunk<
ThunkResult,
MutationThunkArg,
ThunkApiMetaConfig & { state: RootState<any, string, ReducerPath> }
>(`${reducerPath}/executeMutation`, executeEndpoint, {
getPendingMeta() {
return addShouldAutoBatch({ startedTimeStamp: Date.now() })
},
})
const hasTheForce = (options: any): options is { force: boolean } =>
'force' in options
const hasMaxAge = (
options: any,
): options is { ifOlderThan: false | number } => 'ifOlderThan' in options
const prefetch =
<EndpointName extends QueryKeys<Definitions>>(
endpointName: EndpointName,
arg: any,
options: PrefetchOptions,
): ThunkAction<void, any, any, UnknownAction> =>
(dispatch: ThunkDispatch<any, any, any>, getState: () => any) => {
const force = hasTheForce(options) && options.force
const maxAge = hasMaxAge(options) && options.ifOlderThan
const queryAction = (force: boolean = true) => {
const options = { forceRefetch: force, isPrefetch: true }
return (
api.endpoints[endpointName] as ApiEndpointQuery<any, any>
).initiate(arg, options)
}
const latestStateValue = (
api.endpoints[endpointName] as ApiEndpointQuery<any, any>
).select(arg)(getState())
if (force) {
dispatch(queryAction())
} else if (maxAge) {
const lastFulfilledTs = latestStateValue?.fulfilledTimeStamp
if (!lastFulfilledTs) {
dispatch(queryAction())
return
}
const shouldRetrigger =
(Number(new Date()) - Number(new Date(lastFulfilledTs))) / 1000 >=
maxAge
if (shouldRetrigger) {
dispatch(queryAction())
}
} else {
// If prefetching with no options, just let it try
dispatch(queryAction(false))
}
}
function matchesEndpoint(endpointName: string) {
return (action: any): action is UnknownAction =>
action?.meta?.arg?.endpointName === endpointName
}
function buildMatchThunkActions<
Thunk extends
| AsyncThunk<any, QueryThunkArg, ThunkApiMetaConfig>
| AsyncThunk<any, MutationThunkArg, ThunkApiMetaConfig>,
>(thunk: Thunk, endpointName: string) {
return {
matchPending: isAllOf(isPending(thunk), matchesEndpoint(endpointName)),
matchFulfilled: isAllOf(
isFulfilled(thunk),
matchesEndpoint(endpointName),
),
matchRejected: isAllOf(isRejected(thunk), matchesEndpoint(endpointName)),
} as Matchers<Thunk, any>
}
return {
queryThunk,
mutationThunk,
infiniteQueryThunk,
prefetch,
updateQueryData,
upsertQueryData,
patchQueryData,
buildMatchThunkActions,
}
}
export function getNextPageParam(
options: InfiniteQueryConfigOptions<unknown, unknown>,
{ pages, pageParams }: InfiniteData<unknown, unknown>,
): unknown | undefined {
const lastIndex = pages.length - 1
return options.getNextPageParam(
pages[lastIndex],
pages,
pageParams[lastIndex],
pageParams,
)
}
export function getPreviousPageParam(
options: InfiniteQueryConfigOptions<unknown, unknown>,
{ pages, pageParams }: InfiniteData<unknown, unknown>,
): unknown | undefined {
return options.getPreviousPageParam?.(
pages[0],
pages,
pageParams[0],
pageParams,
)
}
export function calculateProvidedByThunk(
action: UnwrapPromise<
| ReturnType<ReturnType<QueryThunk>>
| ReturnType<ReturnType<MutationThunk>>
| ReturnType<ReturnType<InfiniteQueryThunk<any>>>
>,
type: 'providesTags' | 'invalidatesTags',
endpointDefinitions: EndpointDefinitions,
assertTagType: AssertTagTypes,
) {
return calculateProvidedBy(
endpointDefinitions[action.meta.arg.endpointName][
type
] as ResultDescription<any, any, any, any, any>,
isFulfilled(action) ? action.payload : undefined,
isRejectedWithValue(action) ? action.payload : undefined,
action.meta.arg.originalArgs,
'baseQueryMeta' in action.meta ? action.meta.baseQueryMeta : undefined,
assertTagType,
)
}