diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 15a723006d351..34cfc2e0096e1 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -3,6 +3,8 @@ ### Enhancements - The `registerStore` function now accepts an optional `initialState` option value. +- Introduce new `invalidateResolutionForStore` dispatch action for signalling to invalidate the resolution cache for an entire given store. +- Introduce new `invalidateResolutionForStoreSelector` dispatch action for signalling to invalidate the resolution cache for a store selector (and all variations of arguments on that selector). ### Bug Fix diff --git a/packages/data/src/store/actions.js b/packages/data/src/store/actions.js index e4d887ee3e58a..b7bd9aa805738 100644 --- a/packages/data/src/store/actions.js +++ b/packages/data/src/store/actions.js @@ -53,3 +53,39 @@ export function invalidateResolution( reducerKey, selectorName, args ) { args, }; } + +/** + * Returns an action object used in signalling that the resolution cache for a + * given reducerKey should be invalidated. + * + * @param {string} reducerKey Registered store reducer key. + * + * @return {Object} Action object. + */ +export function invalidateResolutionForStore( reducerKey ) { + return { + type: 'INVALIDATE_RESOLUTION_FOR_STORE', + reducerKey, + }; +} + +/** + * Returns an action object used in signalling that the resolution cache for a + * given reducerKey and selectorName should be invalidated. + * + * @param {string} reducerKey Registered store reducer key. + * @param {string} selectorName Name of selector for which all resolvers should + * be invalidated. + * + * @return {Object} Action object. + */ +export function invalidateResolutionForStoreSelector( + reducerKey, + selectorName +) { + return { + type: 'INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR', + reducerKey, + selectorName, + }; +} diff --git a/packages/data/src/store/reducer.js b/packages/data/src/store/reducer.js index 69cd865766234..cd043f753f833 100644 --- a/packages/data/src/store/reducer.js +++ b/packages/data/src/store/reducer.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { flowRight } from 'lodash'; +import { flowRight, omit, has } from 'lodash'; import EquivalentKeyMap from 'equivalent-key-map'; /** @@ -10,7 +10,8 @@ import EquivalentKeyMap from 'equivalent-key-map'; import { onSubKey } from './utils'; /** - * Reducer function returning next state for selector resolution, object form: + * Reducer function returning next state for selector resolution of + * subkeys, object form: * * reducerKey -> selectorName -> EquivalentKeyMap * @@ -19,7 +20,7 @@ import { onSubKey } from './utils'; * * @returns {Object} Next state. */ -const isResolved = flowRight( [ +const subKeysIsResolved = flowRight( [ onSubKey( 'reducerKey' ), onSubKey( 'selectorName' ), ] )( ( state = new EquivalentKeyMap(), action ) => { @@ -37,8 +38,41 @@ const isResolved = flowRight( [ return nextState; } } - return state; } ); +/** + * Reducer function returning next state for selector resolution, object form: + * + * reducerKey -> selectorName -> EquivalentKeyMap + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Next state. + */ +const isResolved = ( state = {}, action ) => { + switch ( action.type ) { + case 'INVALIDATE_RESOLUTION_FOR_STORE': + return has( state, action.reducerKey ) ? + omit( state, [ action.reducerKey ] ) : + state; + case 'INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR': + return has( state, [ action.reducerKey, action.selectorName ] ) ? + { + ...state, + [ action.reducerKey ]: omit( + state[ action.reducerKey ], + [ action.selectorName ] + ), + } : + state; + case 'START_RESOLUTION': + case 'FINISH_RESOLUTION': + case 'INVALIDATE_RESOLUTION': + return subKeysIsResolved( state, action ); + } + return state; +}; + export default isResolved; diff --git a/packages/data/src/store/test/reducer.js b/packages/data/src/store/test/reducer.js index 186703c7738a2..eab156e89a71e 100644 --- a/packages/data/src/store/test/reducer.js +++ b/packages/data/src/store/test/reducer.js @@ -93,4 +93,53 @@ describe( 'reducer', () => { expect( state.test.getFoo.get( [ 'post' ] ) ).toBe( false ); expect( state.test.getFoo.get( [ 'block' ] ) ).toBe( true ); } ); + + it( 'should remove invalidation for store level and leave others ' + + 'intact', () => { + const original = reducer( undefined, { + type: 'FINISH_RESOLUTION', + reducerKey: 'testA', + selectorName: 'getFoo', + args: [ 'post' ], + } ); + let state = reducer( deepFreeze( original ), { + type: 'FINISH_RESOLUTION', + reducerKey: 'testB', + selectorName: 'getBar', + args: [ 'postBar' ], + } ); + state = reducer( deepFreeze( state ), { + type: 'INVALIDATE_RESOLUTION_FOR_STORE', + reducerKey: 'testA', + } ); + + expect( state.testA ).toBeUndefined(); + // { testB: { getBar: EquivalentKeyMap( [] => false ) } } + expect( state.testB.getBar.get( [ 'postBar' ] ) ).toBe( false ); + } ); + + it( 'should remove invalidation for store and selector name level and ' + + 'leave other selectors at store level intact', () => { + const original = reducer( undefined, { + type: 'FINISH_RESOLUTION', + reducerKey: 'test', + selectorName: 'getFoo', + args: [ 'post' ], + } ); + let state = reducer( deepFreeze( original ), { + type: 'FINISH_RESOLUTION', + reducerKey: 'test', + selectorName: 'getBar', + args: [ 'postBar' ], + } ); + state = reducer( deepFreeze( state ), { + type: 'INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR', + reducerKey: 'test', + selectorName: 'getBar', + } ); + + expect( state.test.getBar ).toBeUndefined(); + // { test: { getFoo: EquivalentKeyMap( [] => false ) } } + expect( state.test.getFoo.get( [ 'post' ] ) ).toBe( false ); + } ); } );