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

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

Added visualizations

  • Property mode set to 100644
File size: 22.1 KB
Line 
1import type { PayloadAction } from '@reduxjs/toolkit'
2import {
3 combineReducers,
4 createAction,
5 createSlice,
6 isAnyOf,
7 isFulfilled,
8 isRejectedWithValue,
9 createNextState,
10 prepareAutoBatched,
11 SHOULD_AUTOBATCH,
12 nanoid,
13} from './rtkImports'
14import type {
15 QuerySubstateIdentifier,
16 QuerySubState,
17 MutationSubstateIdentifier,
18 MutationSubState,
19 MutationState,
20 QueryState,
21 InvalidationState,
22 Subscribers,
23 QueryCacheKey,
24 SubscriptionState,
25 ConfigState,
26 InfiniteQuerySubState,
27 InfiniteQueryDirection,
28} from './apiState'
29import {
30 STATUS_FULFILLED,
31 STATUS_PENDING,
32 QueryStatus,
33 STATUS_REJECTED,
34 STATUS_UNINITIALIZED,
35} from './apiState'
36import type {
37 AllQueryKeys,
38 QueryArgFromAnyQueryDefinition,
39 DataFromAnyQueryDefinition,
40 InfiniteQueryThunk,
41 MutationThunk,
42 QueryThunk,
43 QueryThunkArg,
44} from './buildThunks'
45import { calculateProvidedByThunk } from './buildThunks'
46import {
47 ENDPOINT_QUERY,
48 isInfiniteQueryDefinition,
49 type AssertTagTypes,
50 type EndpointDefinitions,
51 type FullTagDescription,
52 type QueryDefinition,
53} from '../endpointDefinitions'
54import type { Patch } from 'immer'
55import { applyPatches, original, isDraft } from '../utils/immerImports'
56import { onFocus, onFocusLost, onOffline, onOnline } from './setupListeners'
57import {
58 isDocumentVisible,
59 isOnline,
60 copyWithStructuralSharing,
61} from '../utils'
62import type { ApiContext } from '../apiTypes'
63import { isUpsertQuery } from './buildInitiate'
64import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
65import type { UnwrapPromise } from '../tsHelpers'
66import { getCurrent } from '../utils/getCurrent'
67
68/**
69 * A typesafe single entry to be upserted into the cache
70 */
71export type NormalizedQueryUpsertEntry<
72 Definitions extends EndpointDefinitions,
73 EndpointName extends AllQueryKeys<Definitions>,
74> = {
75 endpointName: EndpointName
76 arg: QueryArgFromAnyQueryDefinition<Definitions, EndpointName>
77 value: DataFromAnyQueryDefinition<Definitions, EndpointName>
78}
79
80/**
81 * The internal version that is not typesafe since we can't carry the generics through `createSlice`
82 */
83type NormalizedQueryUpsertEntryPayload = {
84 endpointName: string
85 arg: unknown
86 value: unknown
87}
88
89export type ProcessedQueryUpsertEntry = {
90 queryDescription: QueryThunkArg
91 value: unknown
92}
93
94/**
95 * A typesafe representation of a util action creator that accepts cache entry descriptions to upsert
96 */
97export type UpsertEntries<Definitions extends EndpointDefinitions> = (<
98 EndpointNames extends Array<AllQueryKeys<Definitions>>,
99>(
100 entries: [
101 ...{
102 [I in keyof EndpointNames]: NormalizedQueryUpsertEntry<
103 Definitions,
104 EndpointNames[I]
105 >
106 },
107 ],
108) => PayloadAction<NormalizedQueryUpsertEntryPayload[]>) & {
109 match: (
110 action: unknown,
111 ) => action is PayloadAction<NormalizedQueryUpsertEntryPayload[]>
112}
113
114function updateQuerySubstateIfExists(
115 state: QueryState<any>,
116 queryCacheKey: QueryCacheKey,
117 update: (substate: QuerySubState<any> | InfiniteQuerySubState<any>) => void,
118) {
119 const substate = state[queryCacheKey]
120 if (substate) {
121 update(substate)
122 }
123}
124
125export function getMutationCacheKey(
126 id:
127 | MutationSubstateIdentifier
128 | { requestId: string; arg: { fixedCacheKey?: string | undefined } },
129): string
130export function getMutationCacheKey(id: {
131 fixedCacheKey?: string
132 requestId?: string
133}): string | undefined
134
135export function getMutationCacheKey(
136 id:
137 | { fixedCacheKey?: string; requestId?: string }
138 | MutationSubstateIdentifier
139 | { requestId: string; arg: { fixedCacheKey?: string | undefined } },
140): string | undefined {
141 return ('arg' in id ? id.arg.fixedCacheKey : id.fixedCacheKey) ?? id.requestId
142}
143
144function updateMutationSubstateIfExists(
145 state: MutationState<any>,
146 id:
147 | MutationSubstateIdentifier
148 | { requestId: string; arg: { fixedCacheKey?: string | undefined } },
149 update: (substate: MutationSubState<any>) => void,
150) {
151 const substate = state[getMutationCacheKey(id)]
152 if (substate) {
153 update(substate)
154 }
155}
156
157const initialState = {} as any
158
159export function buildSlice({
160 reducerPath,
161 queryThunk,
162 mutationThunk,
163 serializeQueryArgs,
164 context: {
165 endpointDefinitions: definitions,
166 apiUid,
167 extractRehydrationInfo,
168 hasRehydrationInfo,
169 },
170 assertTagType,
171 config,
172}: {
173 reducerPath: string
174 queryThunk: QueryThunk
175 infiniteQueryThunk: InfiniteQueryThunk<any>
176 mutationThunk: MutationThunk
177 serializeQueryArgs: InternalSerializeQueryArgs
178 context: ApiContext<EndpointDefinitions>
179 assertTagType: AssertTagTypes
180 config: Omit<
181 ConfigState<string>,
182 'online' | 'focused' | 'middlewareRegistered'
183 >
184}) {
185 const resetApiState = createAction(`${reducerPath}/resetApiState`)
186
187 function writePendingCacheEntry(
188 draft: QueryState<any>,
189 arg: QueryThunkArg,
190 upserting: boolean,
191 meta: {
192 arg: QueryThunkArg
193 requestId: string
194 // requestStatus: 'pending'
195 } & { startedTimeStamp: number },
196 ) {
197 draft[arg.queryCacheKey] ??= {
198 status: STATUS_UNINITIALIZED,
199 endpointName: arg.endpointName,
200 }
201
202 updateQuerySubstateIfExists(draft, arg.queryCacheKey, (substate) => {
203 substate.status = STATUS_PENDING
204
205 substate.requestId =
206 upserting && substate.requestId
207 ? // for `upsertQuery` **updates**, keep the current `requestId`
208 substate.requestId
209 : // for normal queries or `upsertQuery` **inserts** always update the `requestId`
210 meta.requestId
211 if (arg.originalArgs !== undefined) {
212 substate.originalArgs = arg.originalArgs
213 }
214 substate.startedTimeStamp = meta.startedTimeStamp
215
216 const endpointDefinition = definitions[meta.arg.endpointName]
217
218 if (isInfiniteQueryDefinition(endpointDefinition) && 'direction' in arg) {
219 ;(substate as InfiniteQuerySubState<any>).direction =
220 arg.direction as InfiniteQueryDirection
221 }
222 })
223 }
224
225 function writeFulfilledCacheEntry(
226 draft: QueryState<any>,
227 meta: { arg: QueryThunkArg; requestId: string } & {
228 fulfilledTimeStamp: number
229 baseQueryMeta: unknown
230 },
231 payload: unknown,
232 upserting: boolean,
233 ) {
234 updateQuerySubstateIfExists(draft, meta.arg.queryCacheKey, (substate) => {
235 if (substate.requestId !== meta.requestId && !upserting) return
236 const { merge } = definitions[meta.arg.endpointName] as QueryDefinition<
237 any,
238 any,
239 any,
240 any
241 >
242 substate.status = STATUS_FULFILLED
243
244 if (merge) {
245 if (substate.data !== undefined) {
246 const { fulfilledTimeStamp, arg, baseQueryMeta, requestId } = meta
247 // There's existing cache data. Let the user merge it in themselves.
248 // We're already inside an Immer-powered reducer, and the user could just mutate `substate.data`
249 // themselves inside of `merge()`. But, they might also want to return a new value.
250 // Try to let Immer figure that part out, save the result, and assign it to `substate.data`.
251 let newData = createNextState(substate.data, (draftSubstateData) => {
252 // As usual with Immer, you can mutate _or_ return inside here, but not both
253 return merge(draftSubstateData, payload, {
254 arg: arg.originalArgs,
255 baseQueryMeta,
256 fulfilledTimeStamp,
257 requestId,
258 })
259 })
260 substate.data = newData
261 } else {
262 // Presumably a fresh request. Just cache the response data.
263 substate.data = payload
264 }
265 } else {
266 // Assign or safely update the cache data.
267 substate.data =
268 (definitions[meta.arg.endpointName].structuralSharing ?? true)
269 ? copyWithStructuralSharing(
270 isDraft(substate.data)
271 ? original(substate.data)
272 : substate.data,
273 payload,
274 )
275 : payload
276 }
277
278 delete substate.error
279 substate.fulfilledTimeStamp = meta.fulfilledTimeStamp
280 })
281 }
282
283 const querySlice = createSlice({
284 name: `${reducerPath}/queries`,
285 initialState: initialState as QueryState<any>,
286 reducers: {
287 removeQueryResult: {
288 reducer(
289 draft,
290 {
291 payload: { queryCacheKey },
292 }: PayloadAction<QuerySubstateIdentifier>,
293 ) {
294 delete draft[queryCacheKey]
295 },
296 prepare: prepareAutoBatched<QuerySubstateIdentifier>(),
297 },
298 cacheEntriesUpserted: {
299 reducer(
300 draft,
301 action: PayloadAction<
302 ProcessedQueryUpsertEntry[],
303 string,
304 { RTK_autoBatch: boolean; requestId: string; timestamp: number }
305 >,
306 ) {
307 for (const entry of action.payload) {
308 const { queryDescription: arg, value } = entry
309 writePendingCacheEntry(draft, arg, true, {
310 arg,
311 requestId: action.meta.requestId,
312 startedTimeStamp: action.meta.timestamp,
313 })
314
315 writeFulfilledCacheEntry(
316 draft,
317 {
318 arg,
319 requestId: action.meta.requestId,
320 fulfilledTimeStamp: action.meta.timestamp,
321 baseQueryMeta: {},
322 },
323 value,
324 // We know we're upserting here
325 true,
326 )
327 }
328 },
329 prepare: (payload: NormalizedQueryUpsertEntryPayload[]) => {
330 const queryDescriptions: ProcessedQueryUpsertEntry[] = payload.map(
331 (entry) => {
332 const { endpointName, arg, value } = entry
333 const endpointDefinition = definitions[endpointName]
334 const queryDescription: QueryThunkArg = {
335 type: ENDPOINT_QUERY as 'query',
336 endpointName,
337 originalArgs: entry.arg,
338 queryCacheKey: serializeQueryArgs({
339 queryArgs: arg,
340 endpointDefinition,
341 endpointName,
342 }),
343 }
344 return { queryDescription, value }
345 },
346 )
347
348 const result = {
349 payload: queryDescriptions,
350 meta: {
351 [SHOULD_AUTOBATCH]: true,
352 requestId: nanoid(),
353 timestamp: Date.now(),
354 },
355 }
356 return result
357 },
358 },
359 queryResultPatched: {
360 reducer(
361 draft,
362 {
363 payload: { queryCacheKey, patches },
364 }: PayloadAction<
365 QuerySubstateIdentifier & { patches: readonly Patch[] }
366 >,
367 ) {
368 updateQuerySubstateIfExists(draft, queryCacheKey, (substate) => {
369 substate.data = applyPatches(substate.data as any, patches.concat())
370 })
371 },
372 prepare: prepareAutoBatched<
373 QuerySubstateIdentifier & { patches: readonly Patch[] }
374 >(),
375 },
376 },
377 extraReducers(builder) {
378 builder
379 .addCase(queryThunk.pending, (draft, { meta, meta: { arg } }) => {
380 const upserting = isUpsertQuery(arg)
381 writePendingCacheEntry(draft, arg, upserting, meta)
382 })
383 .addCase(queryThunk.fulfilled, (draft, { meta, payload }) => {
384 const upserting = isUpsertQuery(meta.arg)
385 writeFulfilledCacheEntry(draft, meta, payload, upserting)
386 })
387 .addCase(
388 queryThunk.rejected,
389 (draft, { meta: { condition, arg, requestId }, error, payload }) => {
390 updateQuerySubstateIfExists(
391 draft,
392 arg.queryCacheKey,
393 (substate) => {
394 if (condition) {
395 // request was aborted due to condition (another query already running)
396 } else {
397 // request failed
398 if (substate.requestId !== requestId) return
399 substate.status = STATUS_REJECTED
400 substate.error = (payload ?? error) as any
401 }
402 },
403 )
404 },
405 )
406 .addMatcher(hasRehydrationInfo, (draft, action) => {
407 const { queries } = extractRehydrationInfo(action)!
408 for (const [key, entry] of Object.entries(queries)) {
409 if (
410 // do not rehydrate entries that were currently in flight.
411 entry?.status === STATUS_FULFILLED ||
412 entry?.status === STATUS_REJECTED
413 ) {
414 draft[key] = entry
415 }
416 }
417 })
418 },
419 })
420 const mutationSlice = createSlice({
421 name: `${reducerPath}/mutations`,
422 initialState: initialState as MutationState<any>,
423 reducers: {
424 removeMutationResult: {
425 reducer(draft, { payload }: PayloadAction<MutationSubstateIdentifier>) {
426 const cacheKey = getMutationCacheKey(payload)
427 if (cacheKey in draft) {
428 delete draft[cacheKey]
429 }
430 },
431 prepare: prepareAutoBatched<MutationSubstateIdentifier>(),
432 },
433 },
434 extraReducers(builder) {
435 builder
436 .addCase(
437 mutationThunk.pending,
438 (draft, { meta, meta: { requestId, arg, startedTimeStamp } }) => {
439 if (!arg.track) return
440
441 draft[getMutationCacheKey(meta)] = {
442 requestId,
443 status: STATUS_PENDING,
444 endpointName: arg.endpointName,
445 startedTimeStamp,
446 }
447 },
448 )
449 .addCase(mutationThunk.fulfilled, (draft, { payload, meta }) => {
450 if (!meta.arg.track) return
451
452 updateMutationSubstateIfExists(draft, meta, (substate) => {
453 if (substate.requestId !== meta.requestId) return
454 substate.status = STATUS_FULFILLED
455 substate.data = payload
456 substate.fulfilledTimeStamp = meta.fulfilledTimeStamp
457 })
458 })
459 .addCase(mutationThunk.rejected, (draft, { payload, error, meta }) => {
460 if (!meta.arg.track) return
461
462 updateMutationSubstateIfExists(draft, meta, (substate) => {
463 if (substate.requestId !== meta.requestId) return
464
465 substate.status = STATUS_REJECTED
466 substate.error = (payload ?? error) as any
467 })
468 })
469 .addMatcher(hasRehydrationInfo, (draft, action) => {
470 const { mutations } = extractRehydrationInfo(action)!
471 for (const [key, entry] of Object.entries(mutations)) {
472 if (
473 // do not rehydrate entries that were currently in flight.
474 (entry?.status === STATUS_FULFILLED ||
475 entry?.status === STATUS_REJECTED) &&
476 // only rehydrate endpoints that were persisted using a `fixedCacheKey`
477 key !== entry?.requestId
478 ) {
479 draft[key] = entry
480 }
481 }
482 })
483 },
484 })
485
486 type CalculateProvidedByAction = UnwrapPromise<
487 | ReturnType<ReturnType<QueryThunk>>
488 | ReturnType<ReturnType<InfiniteQueryThunk<any>>>
489 >
490
491 const initialInvalidationState: InvalidationState<string> = {
492 tags: {},
493 keys: {},
494 }
495
496 const invalidationSlice = createSlice({
497 name: `${reducerPath}/invalidation`,
498 initialState: initialInvalidationState,
499 reducers: {
500 updateProvidedBy: {
501 reducer(
502 draft,
503 action: PayloadAction<
504 Array<{
505 queryCacheKey: QueryCacheKey
506 providedTags: readonly FullTagDescription<string>[]
507 }>
508 >,
509 ) {
510 for (const { queryCacheKey, providedTags } of action.payload) {
511 removeCacheKeyFromTags(draft, queryCacheKey)
512
513 for (const { type, id } of providedTags) {
514 const subscribedQueries = ((draft.tags[type] ??= {})[
515 id || '__internal_without_id'
516 ] ??= [])
517 const alreadySubscribed =
518 subscribedQueries.includes(queryCacheKey)
519 if (!alreadySubscribed) {
520 subscribedQueries.push(queryCacheKey)
521 }
522 }
523
524 // Remove readonly from the providedTags array
525 draft.keys[queryCacheKey] =
526 providedTags as FullTagDescription<string>[]
527 }
528 },
529 prepare: prepareAutoBatched<
530 Array<{
531 queryCacheKey: QueryCacheKey
532 providedTags: readonly FullTagDescription<string>[]
533 }>
534 >(),
535 },
536 },
537 extraReducers(builder) {
538 builder
539 .addCase(
540 querySlice.actions.removeQueryResult,
541 (draft, { payload: { queryCacheKey } }) => {
542 removeCacheKeyFromTags(draft, queryCacheKey)
543 },
544 )
545 .addMatcher(hasRehydrationInfo, (draft, action) => {
546 const { provided } = extractRehydrationInfo(action)!
547 for (const [type, incomingTags] of Object.entries(
548 provided.tags ?? {},
549 )) {
550 for (const [id, cacheKeys] of Object.entries(incomingTags)) {
551 const subscribedQueries = ((draft.tags[type] ??= {})[
552 id || '__internal_without_id'
553 ] ??= [])
554 for (const queryCacheKey of cacheKeys) {
555 const alreadySubscribed =
556 subscribedQueries.includes(queryCacheKey)
557 if (!alreadySubscribed) {
558 subscribedQueries.push(queryCacheKey)
559 }
560 draft.keys[queryCacheKey] = provided.keys[queryCacheKey]
561 }
562 }
563 }
564 })
565 .addMatcher(
566 isAnyOf(isFulfilled(queryThunk), isRejectedWithValue(queryThunk)),
567 (draft, action) => {
568 writeProvidedTagsForQueries(draft, [action])
569 },
570 )
571 .addMatcher(
572 querySlice.actions.cacheEntriesUpserted.match,
573 (draft, action) => {
574 const mockActions: CalculateProvidedByAction[] = action.payload.map(
575 ({ queryDescription, value }) => {
576 return {
577 type: 'UNKNOWN',
578 payload: value,
579 meta: {
580 requestStatus: 'fulfilled',
581 requestId: 'UNKNOWN',
582 arg: queryDescription,
583 },
584 }
585 },
586 )
587 writeProvidedTagsForQueries(draft, mockActions)
588 },
589 )
590 },
591 })
592
593 function removeCacheKeyFromTags(
594 draft: InvalidationState<any>,
595 queryCacheKey: QueryCacheKey,
596 ) {
597 const existingTags = getCurrent(draft.keys[queryCacheKey] ?? [])
598
599 // Delete this cache key from any existing tags that may have provided it
600 for (const tag of existingTags) {
601 const tagType = tag.type
602 const tagId = tag.id ?? '__internal_without_id'
603 const tagSubscriptions = draft.tags[tagType]?.[tagId]
604
605 if (tagSubscriptions) {
606 draft.tags[tagType][tagId] = getCurrent(tagSubscriptions).filter(
607 (qc) => qc !== queryCacheKey,
608 )
609 }
610 }
611
612 delete draft.keys[queryCacheKey]
613 }
614
615 function writeProvidedTagsForQueries(
616 draft: InvalidationState<string>,
617 actions: CalculateProvidedByAction[],
618 ) {
619 const providedByEntries = actions.map((action) => {
620 const providedTags = calculateProvidedByThunk(
621 action,
622 'providesTags',
623 definitions,
624 assertTagType,
625 )
626 const { queryCacheKey } = action.meta.arg
627 return { queryCacheKey, providedTags }
628 })
629
630 invalidationSlice.caseReducers.updateProvidedBy(
631 draft,
632 invalidationSlice.actions.updateProvidedBy(providedByEntries),
633 )
634 }
635
636 // Dummy slice to generate actions
637 const subscriptionSlice = createSlice({
638 name: `${reducerPath}/subscriptions`,
639 initialState: initialState as SubscriptionState,
640 reducers: {
641 updateSubscriptionOptions(
642 d,
643 a: PayloadAction<
644 {
645 endpointName: string
646 requestId: string
647 options: Subscribers[number]
648 } & QuerySubstateIdentifier
649 >,
650 ) {
651 // Dummy
652 },
653 unsubscribeQueryResult(
654 d,
655 a: PayloadAction<{ requestId: string } & QuerySubstateIdentifier>,
656 ) {
657 // Dummy
658 },
659 internal_getRTKQSubscriptions() {},
660 },
661 })
662
663 const internalSubscriptionsSlice = createSlice({
664 name: `${reducerPath}/internalSubscriptions`,
665 initialState: initialState as SubscriptionState,
666 reducers: {
667 subscriptionsUpdated: {
668 reducer(state, action: PayloadAction<Patch[]>) {
669 return applyPatches(state, action.payload)
670 },
671 prepare: prepareAutoBatched<Patch[]>(),
672 },
673 },
674 })
675
676 const configSlice = createSlice({
677 name: `${reducerPath}/config`,
678 initialState: {
679 online: isOnline(),
680 focused: isDocumentVisible(),
681 middlewareRegistered: false,
682 ...config,
683 } as ConfigState<string>,
684 reducers: {
685 middlewareRegistered(state, { payload }: PayloadAction<string>) {
686 state.middlewareRegistered =
687 state.middlewareRegistered === 'conflict' || apiUid !== payload
688 ? 'conflict'
689 : true
690 },
691 },
692 extraReducers: (builder) => {
693 builder
694 .addCase(onOnline, (state) => {
695 state.online = true
696 })
697 .addCase(onOffline, (state) => {
698 state.online = false
699 })
700 .addCase(onFocus, (state) => {
701 state.focused = true
702 })
703 .addCase(onFocusLost, (state) => {
704 state.focused = false
705 })
706 // update the state to be a new object to be picked up as a "state change"
707 // by redux-persist's `autoMergeLevel2`
708 .addMatcher(hasRehydrationInfo, (draft) => ({ ...draft }))
709 },
710 })
711
712 const combinedReducer = combineReducers({
713 queries: querySlice.reducer,
714 mutations: mutationSlice.reducer,
715 provided: invalidationSlice.reducer,
716 subscriptions: internalSubscriptionsSlice.reducer,
717 config: configSlice.reducer,
718 })
719
720 const reducer: typeof combinedReducer = (state, action) =>
721 combinedReducer(resetApiState.match(action) ? undefined : state, action)
722
723 const actions = {
724 ...configSlice.actions,
725 ...querySlice.actions,
726 ...subscriptionSlice.actions,
727 ...internalSubscriptionsSlice.actions,
728 ...mutationSlice.actions,
729 ...invalidationSlice.actions,
730 resetApiState,
731 }
732
733 return { reducer, actions }
734}
735export type SliceActions = ReturnType<typeof buildSlice>['actions']
Note: See TracBrowser for help on using the repository browser.