Skip to content

Commit

Permalink
Merge pull request #2266 from barnabasJ/feature/upsertQueryData
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson authored Aug 28, 2022
2 parents 5e4c51f + 06ee327 commit 72e5673
Show file tree
Hide file tree
Showing 5 changed files with 500 additions and 8 deletions.
16 changes: 14 additions & 2 deletions packages/toolkit/src/query/core/buildInitiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { QueryStatus } from './apiState'
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
import type { Api, ApiContext } from '../apiTypes'
import type { ApiEndpointQuery } from './module'
import type { BaseQueryError } from '../baseQueryTypes'
import type { BaseQueryError, QueryReturnValue } from '../baseQueryTypes'
import type { QueryResultSelectorResult } from './buildSelectors'

declare module './module' {
Expand All @@ -34,10 +34,13 @@ declare module './module' {
}
}

export const forceQueryFnSymbol = Symbol('forceQueryFn')

export interface StartQueryActionCreatorOptions {
subscribe?: boolean
forceRefetch?: boolean | number
subscriptionOptions?: SubscriptionOptions
[forceQueryFnSymbol]?: () => QueryReturnValue
}

type StartQueryActionCreator<
Expand Down Expand Up @@ -259,7 +262,15 @@ Features like automatic cache collection, automatic refetching etc. will not be
endpointDefinition: QueryDefinition<any, any, any, any>
) {
const queryAction: StartQueryActionCreator<any> =
(arg, { subscribe = true, forceRefetch, subscriptionOptions } = {}) =>
(
arg,
{
subscribe = true,
forceRefetch,
subscriptionOptions,
[forceQueryFnSymbol]: forceQueryFn,
} = {}
) =>
(dispatch, getState) => {
const queryCacheKey = serializeQueryArgs({
queryArgs: arg,
Expand All @@ -274,6 +285,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
endpointName,
originalArgs: arg,
queryCacheKey,
[forceQueryFnSymbol]: forceQueryFn,
})
const selector = (
api.endpoints[endpointName] as ApiEndpointQuery<any, any>
Expand Down
9 changes: 8 additions & 1 deletion packages/toolkit/src/query/core/buildSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,14 @@ export function buildSlice({
draft,
meta.arg.queryCacheKey,
(substate) => {
if (substate.requestId !== meta.requestId) return
if (substate.requestId !== meta.requestId) {
if (
substate.fulfilledTimeStamp &&
meta.fulfilledTimeStamp < substate.fulfilledTimeStamp
) {
return
}
}
const { merge } = definitions[
meta.arg.endpointName
] as QueryDefinition<any, any, any, any>
Expand Down
62 changes: 58 additions & 4 deletions packages/toolkit/src/query/core/buildThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import type {
} from '../baseQueryTypes'
import type { RootState, QueryKeys, QuerySubstateIdentifier } from './apiState'
import { QueryStatus } from './apiState'
import type { StartQueryActionCreatorOptions } from './buildInitiate'
import {
forceQueryFnSymbol,
StartQueryActionCreatorOptions,
QueryActionCreatorResult,
} from './buildInitiate'
import type {
AssertTagTypes,
EndpointDefinition,
Expand Down Expand Up @@ -144,6 +148,9 @@ function defaultTransformResponse(baseQueryReturnValue: unknown) {

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,
Expand All @@ -163,6 +170,24 @@ export type UpdateQueryDataThunk<
updateRecipe: Recipe<ResultTypeFrom<Definitions[EndpointName]>>
) => ThunkAction<PatchCollection, PartialState, any, AnyAction>

export type UpsertQueryDataThunk<
Definitions extends EndpointDefinitions,
PartialState
> = <EndpointName extends QueryKeys<Definitions>>(
endpointName: EndpointName,
args: QueryArgFrom<Definitions[EndpointName]>,
value: ResultTypeFrom<Definitions[EndpointName]>
) => ThunkAction<
QueryActionCreatorResult<
Definitions[EndpointName] extends QueryDefinition<any, any, any, any>
? Definitions[EndpointName]
: never
>,
PartialState,
any,
AnyAction
>

/**
* An object returned from dispatching a `api.util.updateQueryData` call.
*/
Expand Down Expand Up @@ -255,6 +280,24 @@ export function buildThunks<
return ret
}

const upsertQueryData: UpsertQueryDataThunk<Definitions, State> =
(endpointName, args, value) => (dispatch) => {
return dispatch(
(
api.endpoints[endpointName] as ApiEndpointQuery<
QueryDefinition<any, any, any, any, any>,
Definitions
>
).initiate(args, {
subscribe: false,
forceRefetch: true,
[forceQueryFnSymbol]: () => ({
data: value,
}),
})
)
}

const executeEndpoint: AsyncThunkPayloadCreator<
ThunkResult,
QueryThunkArg | MutationThunkArg,
Expand Down Expand Up @@ -291,7 +334,12 @@ export function buildThunks<
forced:
arg.type === 'query' ? isForcedQuery(arg, getState()) : undefined,
}
if (endpointDefinition.query) {

const forceQueryFn =
arg.type === 'query' ? arg[forceQueryFnSymbol] : undefined
if (forceQueryFn) {
result = forceQueryFn()
} else if (endpointDefinition.query) {
result = await baseQuery(
endpointDefinition.query(arg.originalArgs),
baseQueryApi,
Expand Down Expand Up @@ -431,12 +479,17 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
const requestState = state[reducerPath]?.queries?.[arg.queryCacheKey]
const fulfilledVal = requestState?.fulfilledTimeStamp

// Don't retry a request that's currently in-flight
if (requestState?.status === 'pending') return false
// Order of these checks matters.
// In order for `upsertQueryData` to successfully run while an existing request is
/// in flight, we have to check `isForcedQuery` before `status === 'pending'`,
// otherwise `queryThunk` will bail out and not run at all.

// if this is forced, continue
if (isForcedQuery(arg, state)) return true

// Don't retry a request that's currently in-flight
if (requestState?.status === 'pending') return false

// Pull from the cache unless we explicitly force refetch or qualify based on time
if (fulfilledVal)
// Value is cached and we didn't specify to refresh, skip it.
Expand Down Expand Up @@ -527,6 +580,7 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
mutationThunk,
prefetch,
updateQueryData,
upsertQueryData,
patchQueryData,
buildMatchThunkActions,
}
Expand Down
12 changes: 11 additions & 1 deletion packages/toolkit/src/query/core/module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/**
* Note: this file should import all other files for type discovery and declaration merging
*/
import type { PatchQueryDataThunk, UpdateQueryDataThunk } from './buildThunks'
import type {
PatchQueryDataThunk,
UpdateQueryDataThunk,
UpsertQueryDataThunk,
} from './buildThunks'
import { buildThunks } from './buildThunks'
import type {
ActionCreatorWithPayload,
Expand Down Expand Up @@ -210,6 +214,10 @@ declare module '../apiTypes' {
Definitions,
RootState<Definitions, string, ReducerPath>
>
upsertQueryData: UpsertQueryDataThunk<
Definitions,
RootState<Definitions, string, ReducerPath>
>
/**
* A Redux thunk that applies a JSON diff/patch array to the cached data for a given query result. This immediately updates the Redux state with those changes.
*
Expand Down Expand Up @@ -416,6 +424,7 @@ export const coreModule = (): Module<CoreModule> => ({
mutationThunk,
patchQueryData,
updateQueryData,
upsertQueryData,
prefetch,
buildMatchThunkActions,
} = buildThunks({
Expand Down Expand Up @@ -444,6 +453,7 @@ export const coreModule = (): Module<CoreModule> => ({
safeAssign(api.util, {
patchQueryData,
updateQueryData,
upsertQueryData,
prefetch,
resetApiState: sliceActions.resetApiState,
})
Expand Down
Loading

0 comments on commit 72e5673

Please sign in to comment.