Skip to content

Commit 8079273

Browse files
committed
First draft implementation of upsertCacheEntries
1 parent 881db12 commit 8079273

File tree

3 files changed

+163
-2
lines changed

3 files changed

+163
-2
lines changed

packages/toolkit/src/query/core/buildSlice.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
isRejectedWithValue,
99
createNextState,
1010
prepareAutoBatched,
11+
SHOULD_AUTOBATCH,
12+
nanoid,
1113
} from './rtkImports'
1214
import type {
1315
QuerySubstateIdentifier,
@@ -21,15 +23,24 @@ import type {
2123
QueryCacheKey,
2224
SubscriptionState,
2325
ConfigState,
26+
QueryKeys,
2427
} from './apiState'
2528
import { QueryStatus } from './apiState'
26-
import type { MutationThunk, QueryThunk, RejectedAction } from './buildThunks'
29+
import type {
30+
MutationThunk,
31+
QueryThunk,
32+
QueryThunkArg,
33+
RejectedAction,
34+
} from './buildThunks'
2735
import { calculateProvidedByThunk } from './buildThunks'
2836
import type {
2937
AssertTagTypes,
38+
DefinitionType,
3039
EndpointDefinitions,
3140
FullTagDescription,
41+
QueryArgFrom,
3242
QueryDefinition,
43+
ResultTypeFrom,
3344
} from '../endpointDefinitions'
3445
import type { Patch } from 'immer'
3546
import { isDraft } from 'immer'
@@ -42,6 +53,44 @@ import {
4253
} from '../utils'
4354
import type { ApiContext } from '../apiTypes'
4455
import { isUpsertQuery } from './buildInitiate'
56+
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
57+
58+
/**
59+
* A typesafe single entry to be upserted into the cache
60+
*/
61+
export type NormalizedQueryUpsertEntry<
62+
Definitions extends EndpointDefinitions,
63+
EndpointName extends QueryKeys<Definitions>,
64+
> = {
65+
endpointName: EndpointName
66+
args: QueryArgFrom<Definitions[EndpointName]>
67+
value: ResultTypeFrom<Definitions[EndpointName]>
68+
}
69+
70+
/**
71+
* The internal version that is not typesafe since we can't carry the generics through `createSlice`
72+
*/
73+
type NormalizedQueryUpsertEntryPayload = {
74+
endpointName: string
75+
args: any
76+
value: any
77+
}
78+
79+
/**
80+
* A typesafe representation of a util action creator that accepts cache entry descriptions to upsert
81+
*/
82+
export type UpsertEntries<Definitions extends EndpointDefinitions> = <
83+
EndpointNames extends Array<QueryKeys<Definitions>>,
84+
>(
85+
entries: [
86+
...{
87+
[I in keyof EndpointNames]: NormalizedQueryUpsertEntry<
88+
Definitions,
89+
EndpointNames[I]
90+
>
91+
},
92+
],
93+
) => PayloadAction<NormalizedQueryUpsertEntryPayload>
4594

4695
function updateQuerySubstateIfExists(
4796
state: QueryState<any>,
@@ -92,6 +141,7 @@ export function buildSlice({
92141
reducerPath,
93142
queryThunk,
94143
mutationThunk,
144+
serializeQueryArgs,
95145
context: {
96146
endpointDefinitions: definitions,
97147
apiUid,
@@ -104,6 +154,7 @@ export function buildSlice({
104154
reducerPath: string
105155
queryThunk: QueryThunk
106156
mutationThunk: MutationThunk
157+
serializeQueryArgs: InternalSerializeQueryArgs
107158
context: ApiContext<EndpointDefinitions>
108159
assertTagType: AssertTagTypes
109160
config: Omit<
@@ -221,6 +272,63 @@ export function buildSlice({
221272
},
222273
prepare: prepareAutoBatched<QuerySubstateIdentifier>(),
223274
},
275+
cacheEntriesUpserted: {
276+
reducer(
277+
draft,
278+
action: PayloadAction<
279+
NormalizedQueryUpsertEntryPayload[],
280+
string,
281+
{
282+
RTK_autoBatch: boolean
283+
requestId: string
284+
timestamp: number
285+
}
286+
>,
287+
) {
288+
for (const entry of action.payload) {
289+
const { endpointName, args, value } = entry
290+
const endpointDefinition = definitions[endpointName]
291+
292+
const arg: QueryThunkArg = {
293+
type: 'query',
294+
endpointName: endpointName,
295+
originalArgs: entry.args,
296+
queryCacheKey: serializeQueryArgs({
297+
queryArgs: args,
298+
endpointDefinition,
299+
endpointName,
300+
}),
301+
}
302+
writePendingCacheEntry(draft, arg, true, {
303+
arg,
304+
requestId: action.meta.requestId,
305+
startedTimeStamp: action.meta.timestamp,
306+
})
307+
308+
writeFulfilledCacheEntry(
309+
draft,
310+
{
311+
arg,
312+
requestId: action.meta.requestId,
313+
fulfilledTimeStamp: action.meta.timestamp,
314+
baseQueryMeta: {},
315+
},
316+
entry.value,
317+
)
318+
}
319+
},
320+
prepare: (payload: NormalizedQueryUpsertEntryPayload[]) => {
321+
const result = {
322+
payload,
323+
meta: {
324+
[SHOULD_AUTOBATCH]: true,
325+
requestId: nanoid(),
326+
timestamp: Date.now(),
327+
},
328+
}
329+
return result
330+
},
331+
},
224332
queryResultPatched: {
225333
reducer(
226334
draft,

packages/toolkit/src/query/core/module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import type {
4747
BuildSelectorsApiEndpointQuery,
4848
} from './buildSelectors'
4949
import { buildSelectors } from './buildSelectors'
50-
import type { SliceActions } from './buildSlice'
50+
import type { SliceActions, UpsertEntries } from './buildSlice'
5151
import { buildSlice } from './buildSlice'
5252
import type {
5353
BuildThunksApiEndpointMutation,
@@ -320,6 +320,9 @@ export interface ApiModules<
320320
* ```
321321
*/
322322
resetApiState: SliceActions['resetApiState']
323+
324+
upsertEntries: UpsertEntries<Definitions>
325+
323326
/**
324327
* A Redux action creator that can be used to manually invalidate cache tags for [automated re-fetching](../../usage/automated-refetching.mdx).
325328
*
@@ -527,6 +530,7 @@ export const coreModule = ({
527530
context,
528531
queryThunk,
529532
mutationThunk,
533+
serializeQueryArgs,
530534
reducerPath,
531535
assertTagType,
532536
config: {
@@ -545,6 +549,7 @@ export const coreModule = ({
545549
upsertQueryData,
546550
prefetch,
547551
resetApiState: sliceActions.resetApiState,
552+
upsertEntries: sliceActions.cacheEntriesUpserted as any,
548553
})
549554
safeAssign(api.internalActions, sliceActions)
550555

packages/toolkit/src/query/tests/optimisticUpserts.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const api = createApi({
2727
},
2828
tagTypes: ['Post'],
2929
endpoints: (build) => ({
30+
getPosts: build.query<Post[], void>({
31+
query: () => '/posts',
32+
}),
3033
post: build.query<Post, string>({
3134
query: (id) => `post/${id}`,
3235
providesTags: ['Post'],
@@ -327,6 +330,51 @@ describe('upsertQueryData', () => {
327330
})
328331
})
329332

333+
describe('upsertEntries', () => {
334+
test('Upserts many entries at once', async () => {
335+
const posts: Post[] = [
336+
{
337+
id: '1',
338+
contents: 'A',
339+
title: 'A',
340+
},
341+
{
342+
id: '2',
343+
contents: 'B',
344+
title: 'B',
345+
},
346+
{
347+
id: '3',
348+
contents: 'C',
349+
title: 'C',
350+
},
351+
]
352+
353+
storeRef.store.dispatch(
354+
api.util.upsertEntries([
355+
{
356+
endpointName: 'getPosts',
357+
args: undefined,
358+
value: posts,
359+
},
360+
...posts.map((post) => ({
361+
endpointName: 'post' as const,
362+
args: post.id,
363+
value: post,
364+
})),
365+
]),
366+
)
367+
368+
const state = storeRef.store.getState()
369+
370+
expect(api.endpoints.getPosts.select()(state).data).toBe(posts)
371+
372+
expect(api.endpoints.post.select('1')(state).data).toBe(posts[0])
373+
expect(api.endpoints.post.select('2')(state).data).toBe(posts[1])
374+
expect(api.endpoints.post.select('3')(state).data).toBe(posts[2])
375+
})
376+
})
377+
330378
describe('full integration', () => {
331379
test('success case', async () => {
332380
baseQuery

0 commit comments

Comments
 (0)