diff --git a/packages/toolkit/src/query/core/apiState.ts b/packages/toolkit/src/query/core/apiState.ts index d73584e11e..086dc26cd4 100644 --- a/packages/toolkit/src/query/core/apiState.ts +++ b/packages/toolkit/src/query/core/apiState.ts @@ -3,6 +3,7 @@ import type { BaseQueryError } from '../baseQueryTypes' import type { BaseEndpointDefinition, EndpointDefinitions, + FullTagDescription, InfiniteQueryDefinition, MutationDefinition, PageParamFrom, @@ -15,14 +16,8 @@ import type { Id, WithRequiredProp } from '../tsHelpers' export type QueryCacheKey = string & { _type: 'queryCacheKey' } export type QuerySubstateIdentifier = { queryCacheKey: QueryCacheKey } export type MutationSubstateIdentifier = - | { - requestId: string - fixedCacheKey?: string - } - | { - requestId?: string - fixedCacheKey: string - } + | { requestId: string; fixedCacheKey?: string } + | { requestId?: string; fixedCacheKey: string } export type RefetchConfigOptions = { refetchOnMountOrArgChange: boolean | number @@ -225,18 +220,15 @@ export type QuerySubState< D extends BaseEndpointDefinition, DataType = ResultTypeFrom, > = Id< - | ({ - status: QueryStatus.fulfilled - } & WithRequiredProp< + | ({ status: QueryStatus.fulfilled } & WithRequiredProp< BaseQuerySubState, 'data' | 'fulfilledTimeStamp' > & { error: undefined }) - | ({ - status: QueryStatus.pending - } & BaseQuerySubState) - | ({ - status: QueryStatus.rejected - } & WithRequiredProp, 'error'>) + | ({ status: QueryStatus.pending } & BaseQuerySubState) + | ({ status: QueryStatus.rejected } & WithRequiredProp< + BaseQuerySubState, + 'error' + >) | { status: QueryStatus.uninitialized originalArgs?: undefined @@ -274,18 +266,17 @@ type BaseMutationSubState> = { } export type MutationSubState> = - | (({ - status: QueryStatus.fulfilled - } & WithRequiredProp< + | (({ status: QueryStatus.fulfilled } & WithRequiredProp< BaseMutationSubState, 'data' | 'fulfilledTimeStamp' >) & { error: undefined }) - | (({ - status: QueryStatus.pending - } & BaseMutationSubState) & { data?: undefined }) - | ({ - status: QueryStatus.rejected - } & WithRequiredProp, 'error'>) + | (({ status: QueryStatus.pending } & BaseMutationSubState) & { + data?: undefined + }) + | ({ status: QueryStatus.rejected } & WithRequiredProp< + BaseMutationSubState, + 'error' + >) | { requestId?: undefined status: QueryStatus.uninitialized @@ -309,10 +300,13 @@ export type CombinedState< } export type InvalidationState = { - [_ in TagTypes]: { - [id: string]: Array - [id: number]: Array + tags: { + [_ in TagTypes]: { + [id: string]: Array + [id: number]: Array + } } + keys: Record>> } export type QueryState = { @@ -346,6 +340,4 @@ export type RootState< Definitions extends EndpointDefinitions, TagTypes extends string, ReducerPath extends string, -> = { - [P in ReducerPath]: CombinedState -} +> = { [P in ReducerPath]: CombinedState } diff --git a/packages/toolkit/src/query/core/buildSelectors.ts b/packages/toolkit/src/query/core/buildSelectors.ts index fbdc07f02d..106b157139 100644 --- a/packages/toolkit/src/query/core/buildSelectors.ts +++ b/packages/toolkit/src/query/core/buildSelectors.ts @@ -199,10 +199,7 @@ export function buildSelectors< function withRequestFlags( substate: T, ): T & RequestStatusFlags { - return { - ...substate, - ...getRequestStatusFlags(substate.status), - } + return { ...substate, ...getRequestStatusFlags(substate.status) } } function selectApiState(rootState: RootState) { @@ -344,7 +341,7 @@ export function buildSelectors< const apiState = state[reducerPath] const toInvalidate = new Set() for (const tag of tags.filter(isNotNullish).map(expandTagDescription)) { - const provided = apiState.provided[tag.type] + const provided = apiState.provided.tags[tag.type] if (!provided) { continue } diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index 3dd824f5b8..b0f09693ef 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -217,10 +217,7 @@ export function buildSlice({ function writeFulfilledCacheEntry( draft: QueryState, - meta: { - arg: QueryThunkArg - requestId: string - } & { + meta: { arg: QueryThunkArg; requestId: string } & { fulfilledTimeStamp: number baseQueryMeta: unknown }, @@ -297,11 +294,7 @@ export function buildSlice({ action: PayloadAction< ProcessedQueryUpsertEntry[], string, - { - RTK_autoBatch: boolean - requestId: string - timestamp: number - } + { RTK_autoBatch: boolean; requestId: string; timestamp: number } >, ) { for (const entry of action.payload) { @@ -488,43 +481,51 @@ export function buildSlice({ | ReturnType>> > + const initialInvalidationState: InvalidationState = { + tags: {}, + keys: {}, + } + const invalidationSlice = createSlice({ name: `${reducerPath}/invalidation`, - initialState: initialState as InvalidationState, + initialState: initialInvalidationState, reducers: { updateProvidedBy: { reducer( draft, - action: PayloadAction<{ - queryCacheKey: QueryCacheKey - providedTags: readonly FullTagDescription[] - }>, + action: PayloadAction< + Array<{ + queryCacheKey: QueryCacheKey + providedTags: readonly FullTagDescription[] + }> + >, ) { - const { queryCacheKey, providedTags } = action.payload + for (const { queryCacheKey, providedTags } of action.payload) { + removeCacheKeyFromTags(draft, queryCacheKey) - 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.tags[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) - } + // Remove readonly from the providedTags array + draft.keys[queryCacheKey] = + providedTags as FullTagDescription[] } }, - prepare: prepareAutoBatched<{ - queryCacheKey: QueryCacheKey - providedTags: readonly FullTagDescription[] - }>(), + prepare: + prepareAutoBatched< + Array<{ + queryCacheKey: QueryCacheKey + providedTags: readonly FullTagDescription[] + }> + >(), }, }, extraReducers(builder) { @@ -532,23 +533,14 @@ export function buildSlice({ .addCase( querySlice.actions.removeQueryResult, (draft, { payload: { queryCacheKey } }) => { - 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) - } - } - } + removeCacheKeyFromTags(draft, queryCacheKey) }, ) .addMatcher(hasRehydrationInfo, (draft, action) => { const { provided } = extractRehydrationInfo(action)! for (const [type, incomingTags] of Object.entries(provided)) { for (const [id, cacheKeys] of Object.entries(incomingTags)) { - const subscribedQueries = ((draft[type] ??= {})[ + const subscribedQueries = ((draft.tags[type] ??= {})[ id || '__internal_without_id' ] ??= []) for (const queryCacheKey of cacheKeys) { @@ -564,48 +556,71 @@ export function buildSlice({ .addMatcher( isAnyOf(isFulfilled(queryThunk), isRejectedWithValue(queryThunk)), (draft, action) => { - writeProvidedTagsForQuery(draft, action) + writeProvidedTagsForQueries(draft, [action]) }, ) .addMatcher( querySlice.actions.cacheEntriesUpserted.match, (draft, action) => { - for (const { queryDescription: arg, value } of action.payload) { - const action: CalculateProvidedByAction = { - type: 'UNKNOWN', - payload: value, - meta: { - requestStatus: 'fulfilled', - requestId: 'UNKNOWN', - arg, - }, - } - - writeProvidedTagsForQuery(draft, action) - } + const mockActions: CalculateProvidedByAction[] = action.payload.map( + ({ queryDescription, value }) => { + return { + type: 'UNKNOWN', + payload: value, + meta: { + requestStatus: 'fulfilled', + requestId: 'UNKNOWN', + arg: queryDescription, + }, + } + }, + ) + writeProvidedTagsForQueries(draft, mockActions) }, ) }, }) - function writeProvidedTagsForQuery( + function removeCacheKeyFromTags( + draft: InvalidationState, + queryCacheKey: QueryCacheKey, + ) { + const existingTags = draft.keys[queryCacheKey] ?? [] + + // Delete this cache key from any existing tags that may have provided it + for (const tag of existingTags) { + const tagType = tag.type + const tagId = tag.id ?? '__internal_without_id' + const tagSubscriptions = draft.tags[tagType]?.[tagId] + + if (tagSubscriptions) { + draft.tags[tagType][tagId] = tagSubscriptions.filter( + (qc) => qc !== queryCacheKey, + ) + } + } + + delete draft.keys[queryCacheKey] + } + + function writeProvidedTagsForQueries( draft: InvalidationState, - action: CalculateProvidedByAction, + actions: CalculateProvidedByAction[], ) { - const providedTags = calculateProvidedByThunk( - action, - 'providesTags', - definitions, - assertTagType, - ) - const { queryCacheKey } = action.meta.arg + const providedByEntries = actions.map((action) => { + const providedTags = calculateProvidedByThunk( + action, + 'providesTags', + definitions, + assertTagType, + ) + const { queryCacheKey } = action.meta.arg + return { queryCacheKey, providedTags } + }) invalidationSlice.caseReducers.updateProvidedBy( draft, - invalidationSlice.actions.updateProvidedBy({ - queryCacheKey, - providedTags, - }), + invalidationSlice.actions.updateProvidedBy(providedByEntries), ) } diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 81ca5d38a7..d03c30ef24 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -166,19 +166,13 @@ type MutationThunkArg = { export type ThunkResult = unknown export type ThunkApiMetaConfig = { - pendingMeta: { - startedTimeStamp: number - [SHOULD_AUTOBATCH]: true - } + pendingMeta: { startedTimeStamp: number; [SHOULD_AUTOBATCH]: true } fulfilledMeta: { fulfilledTimeStamp: number baseQueryMeta: unknown [SHOULD_AUTOBATCH]: true } - rejectedMeta: { - baseQueryMeta: unknown - [SHOULD_AUTOBATCH]: true - } + rejectedMeta: { baseQueryMeta: unknown; [SHOULD_AUTOBATCH]: true } } export type QueryThunk = AsyncThunk< ThunkResult, @@ -320,10 +314,7 @@ type TransformCallback = ( export const addShouldAutoBatch = >( arg: T = {} as T, ): T & { [SHOULD_AUTOBATCH]: true } => { - return { - ...arg, - [SHOULD_AUTOBATCH]: true, - } + return { ...arg, [SHOULD_AUTOBATCH]: true } } export function buildThunks< @@ -382,7 +373,7 @@ export function buildThunks< ) dispatch( - api.internalActions.updateProvidedBy({ queryCacheKey, providedTags }), + api.internalActions.updateProvidedBy([{ queryCacheKey, providedTags }]), ) } @@ -466,9 +457,7 @@ export function buildThunks< ).initiate(arg, { subscribe: false, forceRefetch: true, - [forceQueryFnSymbol]: () => ({ - data: value, - }), + [forceQueryFnSymbol]: () => ({ data: value }), }), ) as UpsertThunkResult @@ -623,10 +612,7 @@ export function buildThunks< finalQueryArg, ) - return { - ...result, - data: transformedResponse, - } + return { ...result, data: transformedResponse } } if ( @@ -793,9 +779,7 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".` return addShouldAutoBatch({ startedTimeStamp: Date.now(), ...(isInfiniteQueryDefinition(endpointDefinition) - ? { - direction: (arg as InfiniteQueryThunkArg).direction, - } + ? { direction: (arg as InfiniteQueryThunkArg).direction } : {}), }) }, diff --git a/packages/toolkit/src/query/tests/optimisticUpdates.test.tsx b/packages/toolkit/src/query/tests/optimisticUpdates.test.tsx index eb9893a3be..afc40254b9 100644 --- a/packages/toolkit/src/query/tests/optimisticUpdates.test.tsx +++ b/packages/toolkit/src/query/tests/optimisticUpdates.test.tsx @@ -60,9 +60,7 @@ const api = createApi({ }), }) -const storeRef = setupApiStore(api, { - ...actionsReducer, -}) +const storeRef = setupApiStore(api, { ...actionsReducer }) describe('basic lifecycle', () => { let onStart = vi.fn(), @@ -96,9 +94,7 @@ describe('basic lifecycle', () => { test('success', async () => { const { result } = renderHook( () => extendedApi.endpoints.test.useMutation(), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) baseQuery.mockResolvedValue('success') @@ -119,9 +115,7 @@ describe('basic lifecycle', () => { test('error', async () => { const { result } = renderHook( () => extendedApi.endpoints.test.useMutation(), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) baseQuery.mockRejectedValueOnce('error') @@ -201,11 +195,7 @@ describe('updateQueryData', () => { test('updates (list) cache values including provided tags, undos that', async () => { baseQuery .mockResolvedValueOnce([ - { - id: '3', - title: 'All about cheese.', - contents: 'TODO', - }, + { id: '3', title: 'All about cheese.', contents: 'TODO' }, ]) .mockResolvedValueOnce(42) const { result } = renderHook(() => api.endpoints.listPosts.useQuery(), { @@ -218,7 +208,7 @@ describe('updateQueryData', () => { provided = storeRef.store.getState().api.provided }) - const provided3 = provided.Post['3'] + const provided3 = provided.tags.Post['3'] let returnValue!: ReturnType> act(() => { @@ -242,7 +232,7 @@ describe('updateQueryData', () => { provided = storeRef.store.getState().api.provided }) - const provided4 = provided.Post['4'] + const provided4 = provided.tags.Post['4'] expect(provided4).toEqual(provided3) @@ -254,7 +244,7 @@ describe('updateQueryData', () => { provided = storeRef.store.getState().api.provided }) - const provided4Next = provided.Post['4'] + const provided4Next = provided.tags.Post['4'] expect(provided4Next).toEqual([]) }) @@ -262,11 +252,7 @@ describe('updateQueryData', () => { test('updates (list) cache values excluding provided tags, undoes that', async () => { baseQuery .mockResolvedValueOnce([ - { - id: '3', - title: 'All about cheese.', - contents: 'TODO', - }, + { id: '3', title: 'All about cheese.', contents: 'TODO' }, ]) .mockResolvedValueOnce(42) const { result } = renderHook(() => api.endpoints.listPosts.useQuery(), { @@ -301,7 +287,7 @@ describe('updateQueryData', () => { provided = storeRef.store.getState().api.provided }) - const provided4 = provided.Post['4'] + const provided4 = provided.tags.Post['4'] expect(provided4).toEqual(undefined) @@ -313,7 +299,7 @@ describe('updateQueryData', () => { provided = storeRef.store.getState().api.provided }) - const provided4Next = provided.Post['4'] + const provided4Next = provided.tags.Post['4'] expect(provided4Next).toEqual(undefined) }) @@ -382,9 +368,7 @@ describe('full integration', () => { query: api.endpoints.post.useQuery('3'), mutation: api.endpoints.updatePost.useMutation(), }), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) await hookWaitFor(() => expect(result.current.query.isSuccess).toBeTruthy()) @@ -433,9 +417,7 @@ describe('full integration', () => { query: api.endpoints.post.useQuery('3'), mutation: api.endpoints.updatePost.useMutation(), }), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) await hookWaitFor(() => expect(result.current.query.isSuccess).toBeTruthy()) diff --git a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx index 946103c5b3..6d64e3554b 100644 --- a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx +++ b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx @@ -41,9 +41,7 @@ const api = createApi({ }, tagTypes: ['Post', 'Folder'], endpoints: (build) => ({ - getPosts: build.query({ - query: () => '/posts', - }), + getPosts: build.query({ query: () => '/posts' }), post: build.query({ query: (id) => `post/${id}`, providesTags: ['Post'], @@ -70,13 +68,7 @@ const api = createApi({ post2: build.query({ queryFn: async (id) => { await delay(20) - return { - data: { - id, - title: 'All about cheese.', - contents: 'TODO', - }, - } + return { data: { id, title: 'All about cheese.', contents: 'TODO' } } }, }), postWithSideEffect: build.query({ @@ -123,9 +115,7 @@ const api = createApi({ }), }) -const storeRef = setupApiStore(api, { - ...actionsReducer, -}) +const storeRef = setupApiStore(api, { ...actionsReducer }) describe('basic lifecycle', () => { let onStart = vi.fn(), @@ -194,9 +184,7 @@ describe('basic lifecycle', () => { test('success', async () => { const { result } = renderHook( () => extendedApi.endpoints.test.useMutation(), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) baseQuery.mockResolvedValue('success') @@ -217,9 +205,7 @@ describe('basic lifecycle', () => { test('error', async () => { const { result } = renderHook( () => extendedApi.endpoints.test.useMutation(), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) baseQuery.mockRejectedValueOnce('error') @@ -298,9 +284,7 @@ describe('upsertQueryData', () => { // is preserved normally after the last subscriber was unmounted const { result, rerender } = renderHook( () => api.endpoints.post.useQuery('4'), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) await hookWaitFor(() => expect(result.current.isError).toBeTruthy()) @@ -387,29 +371,13 @@ describe('upsertQueryData', () => { describe('upsertQueryEntries', () => { const posts: Post[] = [ - { - id: '1', - contents: 'A', - title: 'A', - }, - { - id: '2', - contents: 'B', - title: 'B', - }, - { - id: '3', - contents: 'C', - title: 'C', - }, + { id: '1', contents: 'A', title: 'A' }, + { id: '2', contents: 'B', title: 'B' }, + { id: '3', contents: 'C', title: 'C' }, ] const entriesAction = api.util.upsertQueryEntries([ - { - endpointName: 'getPosts', - arg: undefined, - value: posts, - }, + { endpointName: 'getPosts', arg: undefined, value: posts }, ...posts.map((post) => ({ endpointName: 'postWithSideEffect' as const, arg: post.id, @@ -430,7 +398,7 @@ describe('upsertQueryEntries', () => { ) // Should have added tags - expect(state.api.provided.Post[post.id]).toEqual([ + expect(state.api.provided.tags.Post[post.id]).toEqual([ `postWithSideEffect("${post.id}")`, ]) } @@ -508,9 +476,7 @@ describe('upsertQueryEntries', () => { ) } - render(, { - wrapper: storeRef.wrapper, - }) + render(, { wrapper: storeRef.wrapper }) await waitFor(() => { const { actions } = storeRef.store.getState() @@ -551,9 +517,7 @@ describe('full integration', () => { query: api.endpoints.post.useQuery('3'), mutation: api.endpoints.updatePost.useMutation(), }), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) await hookWaitFor(() => expect(result.current.query.isSuccess).toBeTruthy()) @@ -605,9 +569,7 @@ describe('full integration', () => { query: api.endpoints.post.useQuery('3'), mutation: api.endpoints.updatePost.useMutation(), }), - { - wrapper: storeRef.wrapper, - }, + { wrapper: storeRef.wrapper }, ) await hookWaitFor(() => expect(result.current.query.isSuccess).toBeTruthy())