Repository: reduxjs/redux-toolkit

Problem statement:
UpsertQueryEntries breaks query invalidation when the invalidationBehavior is set to delayed
```ts
// api configuration
invalidationBehavior: "delayed"

// queries
getQuery: build.query<>({
      query:...,
      providesTags: ["QueryTag"]
}),
editQuery: build.mutation<>({
     invalidatesTags: ["QueryTag"]
}),

getSomeUnrelatedListData: buld.query<>({
     query:...
     async onQueryStarted(_, { dispatch, queryFulfilled }) {
        try {
          const res = await queryFulfilled;
          const results = res.data;
          dispatch(
           apiI.util.upsertQueryEntries(
              results.map((result) => ({
                endpointName: "getSomeUnrelatedSingleItemData",
                arg: { id: result.id },
                value: result,
              })),
            ),
          );
        } catch (err) {
          void err;
        }
      },
    }),
}),

getSomeUnrelatedSingleItemData: buld.query<unknown, {id: number}>({
     
})
```


Steps:

1. Trigger fetching the data by calling the getQuery (useGetQueryQuery())
2. Trigger fetching the data by calling the getSomeUnrelatedListData (useGetSomeUnrelatedListDataQuery())
     *  this populates entries for getSomeUnrelatedSingleItemData
3. Trigger getQuery data invalidation by calling the editQuery mutation


Expected result:
data returned by the useGetQueryQuery() is re-fetched

Actual Result:
the data is not re-fetched


```
"@reduxjs/toolkit": "2.4.0",
```


Hints:
I experienced the same issue.

The issue here seems to be that the when `upsertQueryEntries` is called, the upserted "queries" stay in `status: "pending"` forever.
![Image](https://github.com/user-attachments/assets/128bec6d-4d09-4984-bb31-928828ba09c6)

As you can see in the Redux Browser Devtools, these are all upserted query entries that stay in `status: "pending"`.
So it probably seems to be an issue introduced with the performance optimization in https://github.com/reduxjs/redux-toolkit/pull/4561 @markerikson 

Since `delayed` is the default `invalidationBehavior` [since v2](https://redux-toolkit.js.org/usage/migrating-rtk-2#rtk-query-behavior-changes), this potentially affects all default configurations using upserts and invalidations.

In my opinion, an upserted query entry should always directly be in `status: "fulfilled"` (as we just upserted the data for that query as if it was already resolved from the data source).
Yeah, those definitely should _not_ be in `"pending"` state at all.  `upsertQueryEntries`temporarily sets them to `"pending"` to mimic typical behavior, but is supposed to update them to `"fulfilled"` immediately after that.  `"pending"` should never be visible externally.

Could someone provide a small repro that shows this happening?  That would save me some time trying to fix this.
Hey @markerikson , thanks for the fast reply!
Here you go with a small repro: https://stackblitz.com/edit/react-ts-5ne8gqgs?file=Folder.tsx

Patch:
diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts
--- a/packages/toolkit/src/query/core/buildSlice.ts
+++ b/packages/toolkit/src/query/core/buildSlice.ts
@@ -84,7 +84,7 @@ export type ProcessedQueryUpsertEntry = {
 /**
  * A typesafe representation of a util action creator that accepts cache entry descriptions to upsert
  */
-export type UpsertEntries<Definitions extends EndpointDefinitions> = <
+export type UpsertEntries<Definitions extends EndpointDefinitions> = (<
   EndpointNames extends Array<QueryKeys<Definitions>>,
 >(
   entries: [
@@ -95,7 +95,11 @@ export type UpsertEntries<Definitions extends EndpointDefinitions> = <
       >
     },
   ],
-) => PayloadAction<NormalizedQueryUpsertEntryPayload[]>
+) => PayloadAction<NormalizedQueryUpsertEntryPayload[]>) & {
+  match: (
+    action: unknown,
+  ) => action is PayloadAction<NormalizedQueryUpsertEntryPayload[]>
+}
 
 function updateQuerySubstateIfExists(
   state: QueryState<any>,
@@ -212,10 +216,10 @@ export function buildSlice({
       // RTK_autoBatch: true
     },
     payload: unknown,
+    upserting: boolean,
   ) {
     updateQuerySubstateIfExists(draft, meta.arg.queryCacheKey, (substate) => {
-      if (substate.requestId !== meta.requestId && !isUpsertQuery(meta.arg))
-        return
+      if (substate.requestId !== meta.requestId && !upserting) return
       const { merge } = definitions[meta.arg.endpointName] as QueryDefinition<
         any,
         any,
@@ -248,7 +252,7 @@ export function buildSlice({
       } else {
         // Assign or safely update the cache data.
         substate.data =
-          definitions[meta.arg.endpointName].structuralSharing ?? true
+          (definitions[meta.arg.endpointName].structuralSharing ?? true)
             ? copyWithStructuralSharing(
                 isDraft(substate.data)
                   ? original(substate.data)
@@ -307,6 +311,8 @@ export function buildSlice({
                 baseQueryMeta: {},
               },
               value,
+              // We know we're upserting here
+              true,
             )
           }
         },
@@ -365,7 +371,8 @@ export function buildSlice({
           writePendingCacheEntry(draft, arg, upserting, meta)
         })
         .addCase(queryThunk.fulfilled, (draft, { meta, payload }) => {
-          writeFulfilledCacheEntry(draft, meta, payload)
+          const upserting = isUpsertQuery(meta.arg)
+          writeFulfilledCacheEntry(draft, meta, payload, upserting)
         })
         .addCase(
           queryThunk.rejected,


Test patch:
diff --git a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx
--- a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx
+++ b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx
@@ -5,7 +5,13 @@ import {
   hookWaitFor,
   setupApiStore,
 } from '../../tests/utils/helpers'
-import { renderHook, act, waitFor } from '@testing-library/react'
+import {
+  render,
+  renderHook,
+  act,
+  waitFor,
+  screen,
+} from '@testing-library/react'
 import { delay } from 'msw'
 
 interface Post {
@@ -14,6 +20,11 @@ interface Post {
   contents: string
 }
 
+interface FolderT {
+  id: number
+  children: FolderT[]
+}
+
 const baseQuery = vi.fn()
 beforeEach(() => baseQuery.mockReset())
 
@@ -28,7 +39,7 @@ const api = createApi({
         .catch((e: any) => ({ error: e }))
     return { data: result, meta: 'meta' }
   },
-  tagTypes: ['Post'],
+  tagTypes: ['Post', 'Folder'],
   endpoints: (build) => ({
     getPosts: build.query<Post[], void>({
       query: () => '/posts',
@@ -80,6 +91,30 @@ const api = createApi({
       },
       keepUnusedDataFor: 0.01,
     }),
+    getFolder: build.query<FolderT, number>({
+      queryFn: async (args) => {
+        return {
+          data: {
+            id: args,
+            // Folder contains children that are as well folders
+            children: [{ id: 2, children: [] }],
+          },
+        }
+      },
+      providesTags: (result, err, args) => [{ type: 'Folder', id: args }],
+      onQueryStarted: async (args, queryApi) => {
+        const { data } = await queryApi.queryFulfilled
+
+        // Upsert getFolder endpoint with children from response data
+        const upsertData = data.children.map((child) => ({
+          arg: child.id,
+          endpointName: 'getFolder' as const,
+          value: child,
+        }))
+
+        queryApi.dispatch(api.util.upsertQueryEntries(upsertData))
+      },
+    }),
   }),
 })
 
@@ -434,6 +469,56 @@ describe('upsertQueryEntries', () => {
       undefined,
     )
   })
+
+  test('Handles repeated upserts and async lifecycles', async () => {
+    const StateForUpsertFolder = ({ folderId }: { folderId: number }) => {
+      const { status } = api.useGetFolderQuery(folderId)
+
+      return (
+        <>
+          <div>
+            Status getFolder with ID (
+            {folderId === 1 ? 'original request' : 'upserted'}) {folderId}:{' '}
+            <span data-testid={`status-${folderId}`}>{status}</span>
+          </div>
+        </>
+      )
+    }
+
+    const Folder = () => {
+      const { data, isLoading, isError } = api.useGetFolderQuery(1)
+
+      return (
+        <div>
+          <h1>Folders</h1>
+
+          {isLoading && <div>Loading...</div>}
+
+          {isError && <div>Error...</div>}
+
+          <StateForUpsertFolder key={`state-${1}`} folderId={1} />
+          <StateForUpsertFolder key={`state-${2}`} folderId={2} />
+        </div>
+      )
+    }
+
+    render(<Folder />, {
+      wrapper: storeRef.wrapper,
+    })
+
+    await waitFor(() => {
+      const { actions } = storeRef.store.getState()
+      // Inspection:
+      // - 2 inits
+      // - 2 pendings, 2 fulfilleds for the hook queries
+      // - 2 upserts
+      expect(actions.length).toBe(8)
+      expect(
+        actions.filter((a) => api.util.upsertQueryEntries.match(a)).length,
+      ).toBe(2)
+    })
+    expect(screen.getByTestId('status-2').textContent).toBe('fulfilled')
+  })
 })
 
 describe('full integration', () => {