diff --git a/packages/data/src/namespace-store/index.js b/packages/data/src/namespace-store/index.js index 73e6560baf8e9..ae08c4e75c42c 100644 --- a/packages/data/src/namespace-store/index.js +++ b/packages/data/src/namespace-store/index.js @@ -78,9 +78,8 @@ export default function createNamespace( key, options, registry ) { const state = store.__unstableOriginalGetState(); const hasChanged = state !== lastState; lastState = state; - if ( hasChanged ) { - listener(); + listener( key ); } } ); }; diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 15f02180c7b85..6b15dbc872ca8 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -2,8 +2,8 @@ * External dependencies */ import { - without, mapValues, + castArray, } from 'lodash'; /** @@ -41,27 +41,44 @@ import createCoreDataStore from './store'; */ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; - let listeners = []; + const listeners = new Map(); /** * Global listener called for each store's update. + * + * @param {string} storeKey */ - function globalListener() { - listeners.forEach( ( listener ) => listener() ); + function globalListener( storeKey ) { + // Need to convert Map to array here because iterating through the Map + // iterator will pick up any nested subscriptions added DURING iteration. + // So this captures the snapshot of the contents of the map before + // iterating. + const current = Array.from( listeners.entries() ); + for ( const [ dependencies, listener ] of current ) { + if ( dependencies.length === 0 || dependencies.indexOf( storeKey ) > -1 ) { + listener(); + } + } } /** * Subscribe to changes to any data. * - * @param {Function} listener Listener function. + * @param {Function} listener Listener function. + * @param {Array} storeDependencies If included, the listener will only + * be invoked when a store in this array + * has a state change. * * @return {Function} Unsubscribe function. */ - const subscribe = ( listener ) => { - listeners.push( listener ); + const subscribe = ( listener, storeDependencies ) => { + storeDependencies = typeof storeDependencies !== 'undefined' ? + castArray( storeDependencies ) : + []; + listeners.set( storeDependencies, listener ); return () => { - listeners = without( listeners, listener ); + listeners.delete( storeDependencies ); }; }; diff --git a/packages/data/src/test/registry.js b/packages/data/src/test/registry.js index 57b8789e7095c..a4fb9fed8eeeb 100644 --- a/packages/data/src/test/registry.js +++ b/packages/data/src/test/registry.js @@ -494,6 +494,27 @@ describe( 'createRegistry', () => { expect( incrementedValue ).toBe( 3 ); } ); + it( 'only calls listeners registered to the specific store', () => { + const store1 = registry.registerStore( 'store1', { + reducer: ( state ) => state + 1, + } ); + const store2 = registry.registerStore( 'store2', { + reducer: ( state ) => state + 1, + } ); + + const action = { type: 'dummy' }; + + const subscription1 = jest.fn(); + const subscription2 = jest.fn(); + registry.subscribe( subscription1, [ 'store1' ] ); + registry.subscribe( subscription2, [ 'store2' ] ); + + store1.dispatch( action ); + store2.dispatch( action ); + expect( subscription1 ).toHaveBeenCalledTimes( 1 ); + expect( subscription2 ).toHaveBeenCalledTimes( 1 ); + } ); + it( 'snapshots listeners on change, avoiding a later listener if subscribed during earlier callback', () => { const store = registry.registerStore( 'myAwesomeReducer', { reducer: ( state = 0 ) => state + 1, @@ -506,7 +527,6 @@ describe( 'createRegistry', () => { subscribeWithUnsubscribe( firstListener ); store.dispatch( { type: 'dummy' } ); - expect( secondListener ).not.toHaveBeenCalled(); } );