Commit 397d0ed7cac for woocommerce
commit 397d0ed7cac1432272771aabfd1b37f89fb8957a
Author: Anand Rajaram <anandrajaram21@gmail.com>
Date: Wed Aug 26 20:08:36 2026 -0700
Fix stale leaderboard query responses (#67858)
* Fix stale leaderboard query responses
* Add changelog entry for leaderboard query fix
* Fix leaderboard item cache updates
* Document getItems ItemID key migration
* Store leaderboard records in query cache entries
* Fix item selector key types by item type
diff --git a/packages/js/data/changelog/33593-isolate-leaderboard-queries b/packages/js/data/changelog/33593-isolate-leaderboard-queries
new file mode 100644
index 00000000000..612ae47ecc0
--- /dev/null
+++ b/packages/js/data/changelog/33593-isolate-leaderboard-queries
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent stale leaderboard responses from overwriting results for another query.
diff --git a/packages/js/data/src/items/reducer.ts b/packages/js/data/src/items/reducer.ts
index b50e9b3f2de..f6c5fca796b 100644
--- a/packages/js/data/src/items/reducer.ts
+++ b/packages/js/data/src/items/reducer.ts
@@ -24,7 +24,7 @@ const reducer: Reducer< ItemsState, Action > = (
action
) => {
switch ( action.type ) {
- case TYPES.SET_ITEM:
+ case TYPES.SET_ITEM: {
const itemData = state.data[ action.itemType ] || {};
return {
...state,
@@ -39,7 +39,22 @@ const reducer: Reducer< ItemsState, Action > = (
},
},
};
- case TYPES.SET_ITEMS:
+ }
+ case TYPES.SET_ITEMS: {
+ const resourceName = getResourceName(
+ action.itemType,
+ action.query
+ );
+ if ( action.itemType === 'leaderboards' ) {
+ return {
+ ...state,
+ items: {
+ ...state.items,
+ [ resourceName ]: { data: action.items },
+ },
+ };
+ }
+
const ids: Array< ItemID > = [];
const nextItems = action.items.reduce< Record< ItemID, Item > >(
( result, theItem ) => {
@@ -49,10 +64,6 @@ const reducer: Reducer< ItemsState, Action > = (
},
{}
);
- const resourceName = getResourceName(
- action.itemType,
- action.query
- );
return {
...state,
items: {
@@ -67,6 +78,7 @@ const reducer: Reducer< ItemsState, Action > = (
},
},
};
+ }
case TYPES.SET_ITEMS_TOTAL_COUNT:
const totalResourceName = getTotalCountResourceName(
action.itemType,
diff --git a/packages/js/data/src/items/selectors.ts b/packages/js/data/src/items/selectors.ts
index 9285827129a..160afb7ea37 100644
--- a/packages/js/data/src/items/selectors.ts
+++ b/packages/js/data/src/items/selectors.ts
@@ -9,39 +9,42 @@ import createSelector from 'rememo';
import { getResourceName } from '../utils';
import { getTotalCountResourceName } from './utils';
-import { ItemType, ItemsState, Query, ItemInfer } from './types';
+import { ItemType, ItemsState, Query, ItemIDOf, ItemInfer } from './types';
export type getItemsType = < T extends ItemType >(
itemType: T,
query: Query,
- defaultValue?: Map< number, ItemInfer< T > | undefined >
-) => Map< number, ItemInfer< T > | undefined >;
+ defaultValue?: Map< ItemIDOf< T >, ItemInfer< T > | undefined >
+) => Map< ItemIDOf< T >, ItemInfer< T > | undefined >;
type getItemsSelectorType = < T extends ItemType >(
state: ItemsState,
itemType: T,
query: Query,
- defaultValue?: Map< number, ItemInfer< T > | undefined >
-) => Map< number, Map< number, ItemInfer< T > | undefined > >;
+ defaultValue?: Map< ItemIDOf< T >, ItemInfer< T > | undefined >
+) => Map< ItemIDOf< T >, ItemInfer< T > | undefined >;
export const getItems = createSelector< getItemsSelectorType >(
( state, itemType, query, defaultValue = new Map() ) => {
const resourceName = getResourceName( itemType, query );
- let ids;
+ let entries;
if (
state.items[ resourceName ] &&
typeof state.items[ resourceName ] === 'object'
) {
- ids = ( state.items[ resourceName ] as Record< string, number[] > )
- .data;
+ entries = state.items[ resourceName ].data;
}
- if ( ! ids ) {
+ if ( ! entries ) {
return defaultValue;
}
- return ids.reduce( ( map, id: number ) => {
- map.set( id, state.data[ itemType ]?.[ id ] );
+ return entries.reduce( ( map, entry ) => {
+ const isCachedItem = typeof entry === 'object';
+ const item = isCachedItem
+ ? entry
+ : state.data[ itemType ]?.[ entry ];
+ map.set( isCachedItem ? entry.id : entry, item );
return map;
}, new Map() );
},
diff --git a/packages/js/data/src/items/store-aware-utils.ts b/packages/js/data/src/items/store-aware-utils.ts
index f12ee50c14e..2f8bf8cba84 100644
--- a/packages/js/data/src/items/store-aware-utils.ts
+++ b/packages/js/data/src/items/store-aware-utils.ts
@@ -6,11 +6,11 @@ import { select as wpSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
-import { Query } from './types';
+import { LeaderboardItem, Query } from './types';
import { store as itemsStore } from './';
type Options = {
- id: number;
+ id: LeaderboardItem[ 'id' ];
per_page: number;
persisted_query: Query;
filterQuery: Query;
diff --git a/packages/js/data/src/items/test/reducer.ts b/packages/js/data/src/items/test/reducer.ts
index 5b299c6989e..6f4dd80b27b 100644
--- a/packages/js/data/src/items/test/reducer.ts
+++ b/packages/js/data/src/items/test/reducer.ts
@@ -2,6 +2,7 @@
* Internal dependencies
*/
import reducer from '../reducer';
+import { getItems } from '../selectors';
import TYPES from '../action-types';
import { getResourceName } from '../../utils';
import { getTotalCountResourceName } from '../utils';
@@ -106,6 +107,44 @@ describe( 'items reducer', () => {
expect( ( state.data[ itemType ] || {} )[ '1' ] ).toBe( items[ 0 ] );
expect( ( state.data[ itemType ] || {} )[ '2' ] ).toBe( items[ 1 ] );
+ expect( getItems( state, itemType, query ).get( 1 ) ).toBe(
+ items[ 0 ]
+ );
+ } );
+
+ it( 'keeps leaderboard responses isolated by query', () => {
+ const latestQuery = { per_page: 5 };
+ const staleQuery = { per_page: 12 };
+ const latestItems = [ { id: 'products', label: 'five rows' } ];
+ const staleItems = [ { id: 'products', label: 'twelve rows' } ];
+
+ const latestState = reducer( defaultState, {
+ type: TYPES.SET_ITEMS,
+ items: latestItems,
+ itemType: 'leaderboards',
+ query: latestQuery,
+ totalCount: undefined,
+ } );
+ const state = reducer( latestState, {
+ type: TYPES.SET_ITEMS,
+ items: staleItems,
+ itemType: 'leaderboards',
+ query: staleQuery,
+ totalCount: undefined,
+ } );
+
+ expect(
+ state.items[ getResourceName( 'leaderboards', latestQuery ) ]
+ ).toEqual( { data: latestItems } );
+ expect( state.data.leaderboards ).toBeUndefined();
+ expect(
+ getItems( state, 'leaderboards', latestQuery ).get( 'products' )
+ ?.label
+ ).toBe( 'five rows' );
+ expect(
+ getItems( state, 'leaderboards', staleQuery ).get( 'products' )
+ ?.label
+ ).toBe( 'twelve rows' );
} );
it( 'should handle SET_ITEMS_TOTAL_COUNT', () => {
diff --git a/packages/js/data/src/items/types.ts b/packages/js/data/src/items/types.ts
index 1e542cd3efd..c0bcff1c057 100644
--- a/packages/js/data/src/items/types.ts
+++ b/packages/js/data/src/items/types.ts
@@ -7,11 +7,14 @@ type Link = {
href: string;
};
-// Category, Product, Customer item id is a number, and leaderboards item is a string.
-export type ItemID = number | string;
-
export type ItemType = 'categories' | 'products' | 'customers' | 'leaderboards';
+export type ItemIDOf< T extends ItemType > = T extends 'leaderboards'
+ ? LeaderboardItem[ 'id' ]
+ : number;
+
+export type ItemID = ItemIDOf< ItemType >;
+
export type ItemImage = {
id: number;
date_created: string;
@@ -217,9 +220,7 @@ export type ItemInfer< T > = Partial<
};
export type ItemsState = {
- items:
- | Record< string, { data: ItemID[] } | number >
- | Record< string, never >;
+ items: Record< string, { data: Array< ItemID | Item > } | number >;
data: Partial< Record< ItemType, Record< ItemID, Item > > >;
errors: {
[ key: string ]: unknown;
diff --git a/packages/js/data/src/items/utils.ts b/packages/js/data/src/items/utils.ts
index a0b78537027..d1636a6338d 100644
--- a/packages/js/data/src/items/utils.ts
+++ b/packages/js/data/src/items/utils.ts
@@ -2,7 +2,7 @@
* Internal dependencies
*/
import { getResourceName } from '../utils';
-import { ItemInfer, ItemType, Query } from './types';
+import { ItemIDOf, ItemInfer, ItemType, Query } from './types';
import { ItemsSelector } from './';
/**
@@ -22,7 +22,7 @@ export function searchItemsByString< T extends ItemType >(
) {
const { getItems, getItemsError, isResolving } = selector;
- const items: Record< number, ItemInfer< T > | undefined > = {};
+ const items = {} as Record< ItemIDOf< T >, ItemInfer< T > | undefined >;
let isRequesting = false;
let isError = false;
search.forEach( ( searchWord ) => {