diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index f609e3a989..353586236b 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -127,22 +127,28 @@ export function buildSlice({ }, prepare: prepareAutoBatched(), }, - queryResultPatched: { + queryResultsPatched: { reducer( draft, { - payload: { queryCacheKey, patches }, + payload, }: PayloadAction< - QuerySubstateIdentifier & { patches: readonly Patch[] } + Array >, ) { - updateQuerySubstateIfExists(draft, queryCacheKey, (substate) => { - substate.data = applyPatches(substate.data as any, patches.concat()) - }) + for (const { queryCacheKey, patches } of payload) { + updateQuerySubstateIfExists(draft, queryCacheKey, (substate) => { + substate.data = applyPatches( + substate.data as any, + patches.concat(), + ) + }) + } }, - prepare: prepareAutoBatched< - QuerySubstateIdentifier & { patches: readonly Patch[] } - >(), + prepare: + prepareAutoBatched< + Array + >(), }, }, extraReducers(builder) { @@ -330,39 +336,46 @@ export function buildSlice({ name: `${reducerPath}/invalidation`, initialState: initialState as InvalidationState, reducers: { - updateProvidedBy: { + updateProvidedBys: { reducer( draft, - action: PayloadAction<{ - queryCacheKey: QueryCacheKey - providedTags: readonly FullTagDescription[] - }>, + action: PayloadAction< + Array<{ + queryCacheKey: QueryCacheKey + providedTags: readonly FullTagDescription[] + }> + >, ) { - const { queryCacheKey, providedTags } = action.payload - - for (const tagTypeSubscriptions of Object.values(draft)) { - for (const idSubscriptions of Object.values(tagTypeSubscriptions)) { - const foundAt = idSubscriptions.indexOf(queryCacheKey) - if (foundAt !== -1) { - idSubscriptions.splice(foundAt, 1) + for (const { queryCacheKey, providedTags } of action.payload) { + for (const tagTypeSubscriptions of Object.values(draft)) { + for (const idSubscriptions of Object.values( + tagTypeSubscriptions, + )) { + const foundAt = idSubscriptions.indexOf(queryCacheKey) + if (foundAt !== -1) { + idSubscriptions.splice(foundAt, 1) + } } } - } - for (const { type, id } of providedTags) { - const subscribedQueries = ((draft[type] ??= {})[ - id || '__internal_without_id' - ] ??= []) - const alreadySubscribed = subscribedQueries.includes(queryCacheKey) - if (!alreadySubscribed) { - subscribedQueries.push(queryCacheKey) + for (const { type, id } of providedTags) { + const subscribedQueries = ((draft[type] ??= {})[ + id || '__internal_without_id' + ] ??= []) + const alreadySubscribed = + subscribedQueries.includes(queryCacheKey) + if (!alreadySubscribed) { + subscribedQueries.push(queryCacheKey) + } } } }, - prepare: prepareAutoBatched<{ - queryCacheKey: QueryCacheKey - providedTags: readonly FullTagDescription[] - }>(), + prepare: prepareAutoBatched< + Array<{ + queryCacheKey: QueryCacheKey + providedTags: readonly FullTagDescription[] + }> + >(), }, }, extraReducers(builder) { @@ -410,12 +423,14 @@ export function buildSlice({ ) const { queryCacheKey } = action.meta.arg - invalidationSlice.caseReducers.updateProvidedBy( + invalidationSlice.caseReducers.updateProvidedBys( draft, - invalidationSlice.actions.updateProvidedBy({ - queryCacheKey, - providedTags, - }), + invalidationSlice.actions.updateProvidedBys([ + { + queryCacheKey, + providedTags, + }, + ]), ) }, ) diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 2fe616bb7d..d6749e28ea 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -5,7 +5,12 @@ import type { BaseQueryError, QueryReturnValue, } from '../baseQueryTypes' -import type { RootState, QueryKeys, QuerySubstateIdentifier } from './apiState' +import type { + RootState, + QueryKeys, + QuerySubstateIdentifier, + QueryCacheKey, +} from './apiState' import { QueryStatus } from './apiState' import type { StartQueryActionCreatorOptions, @@ -46,6 +51,7 @@ import { HandledError } from '../HandledError' import type { ApiEndpointQuery, PrefetchOptions } from './module' import type { UnwrapPromise } from '../tsHelpers' +import { emplace } from '../../utils' declare module './module' { export interface ApiEndpointQuery< @@ -170,6 +176,20 @@ export type PatchQueryDataThunk< updateProvided?: boolean, ) => ThunkAction +export type PatchQueriesDataThunk< + Definitions extends EndpointDefinitions, + PartialState, +> = ( + patchesByEndpointName: { + [EndpointName in QueryKeys]?: Array<{ + args: QueryArgFrom + patches: readonly Patch[] + updateProvided?: boolean + }> + }, + defaultUpdateProvided?: boolean, +) => ThunkAction + export type UpdateQueryDataThunk< Definitions extends EndpointDefinitions, PartialState, @@ -180,6 +200,35 @@ export type UpdateQueryDataThunk< updateProvided?: boolean, ) => ThunkAction +type PatchCollectionArray = { + [I in keyof InputArray]: PatchCollection +} + +export type UpdateQueriesDataThunk< + Definitions extends EndpointDefinitions, + PartialState, +> = < + EndpointMap extends { + [EndpointName in QueryKeys]?: Array<{ + args: QueryArgFrom + updateRecipe: Recipe> + updateProvided?: boolean + }> + }, +>( + recipesByEndpointName: EndpointMap, + defaultUpdateProvided?: boolean, +) => ThunkAction< + { + [EndpointName in keyof EndpointMap]: EndpointMap[EndpointName] extends any[] + ? PatchCollectionArray + : never + }, + PartialState, + any, + UnknownAction +> + export type UpsertQueryDataThunk< Definitions extends EndpointDefinitions, PartialState, @@ -237,102 +286,211 @@ export function buildThunks< }) { type State = RootState - const patchQueryData: PatchQueryDataThunk = - (endpointName, args, patches, updateProvided) => (dispatch, getState) => { - const endpointDefinition = endpointDefinitions[endpointName] - - const queryCacheKey = serializeQueryArgs({ - queryArgs: args, - endpointDefinition, - endpointName, - }) + const patchQueriesData: PatchQueriesDataThunk = + (patchesByEndpointName, defaultUpdateProvided) => (dispatch, getState) => { + const queryResultPatches: Parameters< + typeof api.internalActions.queryResultsPatched + >[0] = [] + + const arrayified = Object.entries< + | { + args: any + patches: readonly Patch[] + updateProvided?: boolean + }[] + | undefined + >(patchesByEndpointName) + for (const [endpointName, patches] of arrayified) { + if (!patches) continue + for (const { args, patches: endpointPatches } of patches) { + const endpointDefinition = endpointDefinitions[endpointName] + + const queryCacheKey = serializeQueryArgs({ + queryArgs: args, + endpointDefinition, + endpointName, + }) - dispatch( - api.internalActions.queryResultPatched({ queryCacheKey, patches }), - ) + queryResultPatches.push({ queryCacheKey, patches: endpointPatches }) + } + } - if (!updateProvided) { - return + if (queryResultPatches.length) { + dispatch(api.internalActions.queryResultsPatched(queryResultPatches)) } - const newValue = api.endpoints[endpointName].select(args)( - // Work around TS 4.1 mismatch - getState() as RootState, - ) + // now that the state is updated, we can update the tags - const providedTags = calculateProvidedBy( - endpointDefinition.providesTags, - newValue.data, - undefined, - args, - {}, - assertTagType, - ) + const providedPatches: Parameters< + typeof api.internalActions.updateProvidedBys + >[0] = [] - dispatch( - api.internalActions.updateProvidedBy({ queryCacheKey, providedTags }), - ) - } + for (const [endpointName, patches] of arrayified) { + if (!patches) continue + for (const { + args, + updateProvided = defaultUpdateProvided, + } of patches) { + if (!updateProvided) { + continue + } + const endpointDefinition = endpointDefinitions[endpointName] - const updateQueryData: UpdateQueryDataThunk = - (endpointName, args, updateRecipe, updateProvided = true) => - (dispatch, getState) => { - const endpointDefinition = api.endpoints[endpointName] + const queryCacheKey = serializeQueryArgs({ + queryArgs: args, + endpointDefinition, + endpointName, + }) - const currentState = endpointDefinition.select(args)( - // Work around TS 4.1 mismatch - getState() as RootState, - ) + const newValue = api.endpoints[endpointName].select(args)( + // Work around TS 4.1 mismatch + getState() as RootState, + ) - let ret: PatchCollection = { - patches: [], - inversePatches: [], - undo: () => - dispatch( - api.util.patchQueryData( - endpointName, - args, - ret.inversePatches, - updateProvided, - ), - ), + const providedTags = calculateProvidedBy( + endpointDefinition.providesTags, + newValue.data, + undefined, + args, + {}, + assertTagType, + ) + + providedPatches.push({ queryCacheKey, providedTags }) + } } - if (currentState.status === QueryStatus.uninitialized) { - return ret + if (providedPatches.length) { + dispatch(api.internalActions.updateProvidedBys(providedPatches)) } - 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, + } + + const patchQueryData: PatchQueryDataThunk = ( + endpointName, + args, + patches, + updateProvided, + ) => patchQueriesData({ [endpointName]: [{ args, patches, updateProvided }] }) + + const updateQueriesData: UpdateQueriesDataThunk = + (recipesByEndpointName, defaultUpdateProvided = true) => + (dispatch, getState) => { + const ret: Record> = {} + const patchesByEndpointName: Parameters< + PatchQueriesDataThunk + >[0] = {} + const stateCache = new Map() + const arrayified = Object.entries< + | { + args: any + updateRecipe: Recipe + updateProvided?: boolean + }[] + | undefined + >(recipesByEndpointName) + for (const [endpointName, recipes] of arrayified) { + if (!recipes) continue + const endpointPatches: Array<{ + args: any + patches: readonly Patch[] + updateProvided?: boolean + }> = (patchesByEndpointName[endpointName as QueryKeys] ??= + []) + const endpointCollections = (ret[endpointName] ??= []) + for (const [ + idx, + { args, updateRecipe, updateProvided = defaultUpdateProvided }, + ] of recipes.entries()) { + const endpointDefinition = endpointDefinitions[endpointName] + const endpoint = api.endpoints[endpointName] + + const queryCacheKey = serializeQueryArgs({ + queryArgs: args, + endpointDefinition, + endpointName, + }) + + const currentState: ReturnType> = + emplace(stateCache, queryCacheKey, { + insert: () => + endpoint.select(args)( + // Work around TS 4.1 mismatch + getState() as RootState, + ), + }) + + let patchCollection: PatchCollection = { + patches: [], + inversePatches: [], + undo: () => + dispatch( + api.util.patchQueryData( + endpointName as QueryKeys, + args, + patchCollection.inversePatches, + updateProvided, + ), + ), + } + + if (currentState.status === QueryStatus.uninitialized) { + endpointCollections[idx] = patchCollection + continue + } + + let newValue: any + if ('data' in currentState) { + if (isDraftable(currentState.data)) { + const [value, patches, inversePatches] = produceWithPatches( + currentState.data, + updateRecipe, + ) + patchCollection.patches.push(...patches) + patchCollection.inversePatches.push(...inversePatches) + newValue = value + } else { + newValue = updateRecipe(currentState.data) + patchCollection.patches.push({ + op: 'replace', + path: [], + value: newValue, + }) + patchCollection.inversePatches.push({ + op: 'replace', + path: [], + value: currentState.data, + }) + } + // update the state cache with the new value, so that any following recipes will see the updated value + emplace(stateCache, queryCacheKey, { + update: (v) => ({ ...v, data: newValue }), + }) + } + + endpointCollections[idx] = patchCollection + endpointPatches.push({ + args, + patches: patchCollection.patches, + updateProvided, }) } } dispatch( - api.util.patchQueryData( - endpointName, - args, - ret.patches, - updateProvided, - ), + api.util.patchQueriesData(patchesByEndpointName, defaultUpdateProvided), ) - return ret + return ret as any } + const updateQueryData: UpdateQueryDataThunk = + (endpointName, args, updateRecipe, updateProvided = true) => + (dispatch, getState) => + dispatch( + updateQueriesData({ + [endpointName]: [{ args, updateRecipe, updateProvided }], + }), + )[endpointName][0] + const upsertQueryData: UpsertQueryDataThunk = (endpointName, args, value) => (dispatch) => { return dispatch( @@ -663,8 +821,10 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".` mutationThunk, prefetch, updateQueryData, + updateQueriesData, upsertQueryData, patchQueryData, + patchQueriesData, buildMatchThunkActions, } } diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index c237c7b298..c87e2683ca 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -3,8 +3,10 @@ */ import type { PatchQueryDataThunk, + PatchQueriesDataThunk, UpdateQueryDataThunk, UpsertQueryDataThunk, + UpdateQueriesDataThunk, } from './buildThunks' import { buildThunks } from './buildThunks' import type { @@ -249,6 +251,11 @@ declare module '../apiTypes' { RootState > + updateQueriesData: UpdateQueriesDataThunk< + Definitions, + RootState + > + /** * A Redux thunk action creator that, when dispatched, acts as an artificial API request to upsert a value into the cache. * @@ -303,6 +310,11 @@ declare module '../apiTypes' { RootState > + patchQueriesData: PatchQueriesDataThunk< + Definitions, + RootState + > + /** * A Redux action creator that can be dispatched to manually reset the api state completely. This will immediately remove all existing cache entries, and all queries will be considered 'uninitialized'. * @@ -500,7 +512,9 @@ export const coreModule = ({ queryThunk, mutationThunk, patchQueryData, + patchQueriesData, updateQueryData, + updateQueriesData, upsertQueryData, prefetch, buildMatchThunkActions, @@ -531,7 +545,9 @@ export const coreModule = ({ safeAssign(api.util, { patchQueryData, + patchQueriesData, updateQueryData, + updateQueriesData, upsertQueryData, prefetch, resetApiState: sliceActions.resetApiState, diff --git a/packages/toolkit/src/query/defaultSerializeQueryArgs.ts b/packages/toolkit/src/query/defaultSerializeQueryArgs.ts index fcfd63aeb9..6d68e3d175 100644 --- a/packages/toolkit/src/query/defaultSerializeQueryArgs.ts +++ b/packages/toolkit/src/query/defaultSerializeQueryArgs.ts @@ -42,8 +42,4 @@ export type SerializeQueryArgs = (_: { endpointName: string }) => ReturnType -export type InternalSerializeQueryArgs = (_: { - queryArgs: any - endpointDefinition: EndpointDefinition - endpointName: string -}) => QueryCacheKey +export type InternalSerializeQueryArgs = SerializeQueryArgs