From 40bd2599f479d7dec2cda95f4e2320e879ead084 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 7 May 2019 11:48:49 -0400 Subject: [PATCH] implement dependencies for subscribers. This allows consuming code to pass in a list of stores the listener is specifically subscribing to. The listener will only get invoked when for any state changes on the dependent stores. Default behaviour is preserved if no dependency array is passed in. --- packages/data/src/namespace-store/index.js | 3 +- packages/data/src/registry.js | 33 ++++++++++++++++------ packages/data/src/test/registry.js | 22 ++++++++++++++- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/packages/data/src/namespace-store/index.js b/packages/data/src/namespace-store/index.js index 73e6560baf8e9a..ae08c4e75c42cc 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 15f02180c7b859..6b15dbc872ca84 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 57b8789e7095c3..a4fb9fed8eeeb7 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(); } );