diff --git a/package-lock.json b/package-lock.json index 911ece7ee68c00..9769d333d49638 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55117,6 +55117,7 @@ "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", "redux": "^4.1.2", + "rememo": "^4.0.2", "turbo-combine-reducers": "^1.0.2", "use-memo-one": "^1.1.1" }, @@ -67969,6 +67970,7 @@ "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", "redux": "^4.1.2", + "rememo": "^4.0.2", "turbo-combine-reducers": "^1.0.2", "use-memo-one": "^1.1.1" } diff --git a/packages/data/package.json b/packages/data/package.json index b5c7737c2ba767..149c4e18493acc 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -41,6 +41,7 @@ "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", "redux": "^4.1.2", + "rememo": "^4.0.2", "turbo-combine-reducers": "^1.0.2", "use-memo-one": "^1.1.1" }, diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index 43357b766f618e..d9614a7ba35baf 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -430,6 +430,7 @@ function mapResolveSelectors( selectors, store ) { getResolutionState, getResolutionError, hasResolvingSelectors, + countSelectorsByStatus, ...storeSelectors } = selectors; diff --git a/packages/data/src/redux-store/metadata/selectors.js b/packages/data/src/redux-store/metadata/selectors.js index 6cde6ec54b24e1..884ec714efd574 100644 --- a/packages/data/src/redux-store/metadata/selectors.js +++ b/packages/data/src/redux-store/metadata/selectors.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; + /** * Internal dependencies */ @@ -153,3 +158,38 @@ export function hasResolvingSelectors( state ) { ) ); } + +/** + * Retrieves the total number of selectors, grouped per status. + * + * @param {State} state Data state. + * + * @return {Object} Object, containing selector totals by status. + */ +export const countSelectorsByStatus = createSelector( + ( state ) => { + const selectorsByStatus = {}; + + Object.values( state ).forEach( ( selectorState ) => + /** + * This uses the internal `_map` property of `EquivalentKeyMap` for + * optimization purposes, since the `EquivalentKeyMap` implementation + * does not support a `.values()` implementation. + * + * @see https://github.com/aduth/equivalent-key-map + */ + Array.from( selectorState._map.values() ).forEach( + ( resolution ) => { + const currentStatus = resolution[ 1 ]?.status ?? 'error'; + if ( ! selectorsByStatus[ currentStatus ] ) { + selectorsByStatus[ currentStatus ] = 0; + } + selectorsByStatus[ currentStatus ]++; + } + ) + ); + + return selectorsByStatus; + }, + ( state ) => [ state ] +); diff --git a/packages/data/src/redux-store/metadata/test/selectors.js b/packages/data/src/redux-store/metadata/test/selectors.js index 2eea14a7b6059f..11eba0301e8c55 100644 --- a/packages/data/src/redux-store/metadata/test/selectors.js +++ b/packages/data/src/redux-store/metadata/test/selectors.js @@ -356,3 +356,90 @@ describe( 'hasResolvingSelectors', () => { expect( result ).toBe( true ); } ); } ); + +describe( 'countSelectorsByStatus', () => { + let registry; + + beforeEach( () => { + registry = createRegistry(); + registry.registerStore( 'store', { + reducer: ( state = null, action ) => { + if ( action.type === 'RECEIVE' ) { + return action.items; + } + + return state; + }, + selectors: { + getFoo: ( state ) => state, + getBar: ( state ) => state, + getBaz: ( state ) => state, + getFailingFoo: ( state ) => state, + getFailingBar: ( state ) => state, + }, + resolvers: { + getFailingFoo: () => { + throw new Error( 'error fetching' ); + }, + getFailingBar: () => { + throw new Error( 'error fetching' ); + }, + }, + } ); + } ); + + it( 'counts selectors properly by status, excluding missing statuses', () => { + registry.dispatch( 'store' ).startResolution( 'getFoo', [] ); + registry.dispatch( 'store' ).startResolution( 'getBar', [] ); + registry.dispatch( 'store' ).startResolution( 'getBaz', [] ); + registry.dispatch( 'store' ).finishResolution( 'getFoo', [] ); + registry.dispatch( 'store' ).finishResolution( 'getBaz', [] ); + + const { countSelectorsByStatus } = registry.select( 'store' ); + const result = countSelectorsByStatus(); + + expect( result ).toEqual( { + finished: 2, + resolving: 1, + } ); + } ); + + it( 'counts errors properly', async () => { + registry.dispatch( 'store' ).startResolution( 'getFoo', [] ); + await resolve( registry, 'getFailingFoo' ); + await resolve( registry, 'getFailingBar' ); + registry.dispatch( 'store' ).finishResolution( 'getFoo', [] ); + + const { countSelectorsByStatus } = registry.select( 'store' ); + const result = countSelectorsByStatus(); + + expect( result ).toEqual( { + finished: 1, + error: 2, + } ); + } ); + + it( 'applies memoization and returns the same object for the same state', () => { + const { countSelectorsByStatus } = registry.select( 'store' ); + + expect( countSelectorsByStatus() ).toBe( countSelectorsByStatus() ); + + registry.dispatch( 'store' ).startResolution( 'getFoo', [] ); + registry.dispatch( 'store' ).finishResolution( 'getFoo', [] ); + + expect( countSelectorsByStatus() ).toBe( countSelectorsByStatus() ); + } ); + + it( 'returns a new object when different state is provided', () => { + const { countSelectorsByStatus } = registry.select( 'store' ); + + const result1 = countSelectorsByStatus(); + + registry.dispatch( 'store' ).startResolution( 'getFoo', [] ); + registry.dispatch( 'store' ).finishResolution( 'getFoo', [] ); + + const result2 = countSelectorsByStatus(); + + expect( result1 ).not.toBe( result2 ); + } ); +} );