Skip to content

Commit

Permalink
Refactor the core/data store to be independent from the registry (#14634
Browse files Browse the repository at this point in the history
)
  • Loading branch information
youknowriad authored Mar 28, 2019
1 parent d9768ad commit 1e8d781
Show file tree
Hide file tree
Showing 19 changed files with 220 additions and 251 deletions.
8 changes: 6 additions & 2 deletions lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,9 @@ function gutenberg_register_scripts_and_styles() {
);

// TEMPORARY: Core does not (yet) provide persistence migration from the
// introduction of the block editor.
// introduction of the block editor and still calls the data plugins.
// We unset the existing inline scripts first.
$wp_scripts->registered['wp-data']->extra['after'] = array();
wp_add_inline_script(
'wp-data',
implode(
Expand All @@ -254,8 +256,10 @@ function gutenberg_register_scripts_and_styles() {
'( function() {',
' var userId = ' . get_current_user_ID() . ';',
' var storageKey = "WP_DATA_USER_" + userId;',
' wp.data',
' .use( wp.data.plugins.persistence, { storageKey: storageKey } );',
' wp.data.plugins.persistence.__unstableMigrate( { storageKey: storageKey } );',
'} )()',
'} )();',
)
)
);
Expand Down
1 change: 1 addition & 0 deletions lib/packages-dependencies.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
'wp-data' => array(
'lodash',
'wp-compose',
'wp-deprecated',
'wp-element',
'wp-is-shallow-equal',
'wp-priority-queue',
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@babel/runtime": "^7.3.1",
"@wordpress/compose": "file:../compose",
"@wordpress/deprecated": "file:../deprecated",
"@wordpress/element": "file:../element",
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/priority-queue": "file:../priority-queue",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ import {
get,
mapValues,
} from 'lodash';
import combineReducers from 'turbo-combine-reducers';

/**
* WordPress dependencies
*/
import createReduxRoutineMiddleware from '@wordpress/redux-routine';

/**
* Internal dependencies
*/
import promise from './promise-middleware';
import createResolversCacheMiddleware from './resolvers-cache-middleware';
import promise from '../promise-middleware';
import createResolversCacheMiddleware from '../resolvers-cache-middleware';
import metadataReducer from './metadata/reducer';
import * as metadataSelectors from './metadata/selectors';
import * as metadataActions from './metadata/actions';

/**
* Creates a namespace object with a store derived from the reducer given.
Expand All @@ -27,29 +36,46 @@ export default function createNamespace( key, options, registry ) {
const reducer = options.reducer;
const store = createReduxStore( key, options, registry );

let selectors, actions, resolvers;
if ( options.actions ) {
actions = mapActions( options.actions, store );
}
if ( options.selectors ) {
selectors = mapSelectors( options.selectors, store, registry );
}
let resolvers;
const actions = mapActions( {
...metadataActions,
...options.actions,
}, store );
let selectors = mapSelectors( {
...mapValues( metadataSelectors, ( selector ) => ( state, ...args ) => selector( state.metadata, ...args ) ),
...mapValues( options.selectors, ( selector ) => {
if ( selector.isRegistrySelector ) {
const mappedSelector = ( reg ) => ( state, ...args ) => {
return selector( reg )( state.root, ...args );
};
mappedSelector.isRegistrySelector = selector.isRegistrySelector;
return mappedSelector;
}

return ( state, ...args ) => selector( state.root, ...args );
} ),
}, store, registry );
if ( options.resolvers ) {
const fulfillment = getCoreDataFulfillment( registry, key );
const result = mapResolvers( options.resolvers, selectors, fulfillment, store );
const result = mapResolvers( options.resolvers, selectors, store );
resolvers = result.resolvers;
selectors = result.selectors;
}

const getSelectors = () => selectors;
const getActions = () => actions;

// We have some modules monkey-patching the store object
// It's wrong to do so but until we refactor all of our effects to controls
// We need to keep the same "store" instance here.
store.__unstableOriginalGetState = store.getState;
store.getState = () => store.__unstableOriginalGetState().root;

// Customize subscribe behavior to call listeners only on effective change,
// not on every dispatch.
const subscribe = store && function( listener ) {
let lastState = store.getState();
let lastState = store.__unstableOriginalGetState();
store.subscribe( () => {
const state = store.getState();
const state = store.__unstableOriginalGetState();
const hasChanged = state !== lastState;
lastState = state;

Expand Down Expand Up @@ -84,15 +110,36 @@ export default function createNamespace( key, options, registry ) {
* @return {Object} Newly created redux store.
*/
function createReduxStore( key, options, registry ) {
const middlewares = [
createResolversCacheMiddleware( registry, key ),
promise,
];

if ( options.controls ) {
const normalizedControls = mapValues( options.controls, ( control ) => {
return control.isRegistryControl ? control( registry ) : control;
} );
middlewares.push( createReduxRoutineMiddleware( normalizedControls ) );
}

const enhancers = [
applyMiddleware( createResolversCacheMiddleware( registry, key ), promise ),
applyMiddleware( ...middlewares ),
];
if ( typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__ ) {
enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__( { name: key, instanceId: key } ) );
}

const { reducer, initialState } = options;
return createStore( reducer, initialState, flowRight( enhancers ) );
const enhancedReducer = combineReducers( {
metadata: metadataReducer,
root: reducer,
} );

return createStore(
enhancedReducer,
{ root: initialState },
flowRight( enhancers )
);
}

/**
Expand Down Expand Up @@ -120,7 +167,7 @@ function mapSelectors( selectors, store, registry ) {
// direct assignment.
const argsLength = arguments.length;
const args = new Array( argsLength + 1 );
args[ 0 ] = store.getState();
args[ 0 ] = store.__unstableOriginalGetState();
for ( let i = 0; i < argsLength; i++ ) {
args[ i + 1 ] = arguments[ i ];
}
Expand Down Expand Up @@ -151,11 +198,15 @@ function mapActions( actions, store ) {
*
* @param {Object} resolvers Resolvers to register.
* @param {Object} selectors The current selectors to be modified.
* @param {Object} fulfillment Fulfillment implementation functions.
* @param {Object} store The redux store to which the resolvers should be mapped.
* @return {Object} An object containing updated selectors and resolvers.
*/
function mapResolvers( resolvers, selectors, fulfillment, store ) {
function mapResolvers( resolvers, selectors, store ) {
const mappedResolvers = mapValues( resolvers, ( resolver ) => {
const { fulfill: resolverFulfill = resolver } = resolver;
return { ...resolver, fulfill: resolverFulfill };
} );

const mapSelector = ( selector, selectorName ) => {
const resolver = resolvers[ selectorName ];
if ( ! resolver ) {
Expand All @@ -169,68 +220,43 @@ function mapResolvers( resolvers, selectors, fulfillment, store ) {
return;
}

if ( fulfillment.hasStarted( selectorName, args ) ) {
const { metadata } = store.__unstableOriginalGetState();
if ( metadataSelectors.hasStartedResolution( metadata, selectorName, args ) ) {
return;
}

fulfillment.start( selectorName, args );
await fulfillment.fulfill( selectorName, ...args );
fulfillment.finish( selectorName, args );
store.dispatch( metadataActions.startResolution( selectorName, args ) );
await fulfillResolver( store, mappedResolvers, selectorName, ...args );
store.dispatch( metadataActions.finishResolution( selectorName, args ) );
}

fulfillSelector( ...args );
return selector( ...args );
};
};

const mappedResolvers = mapValues( resolvers, ( resolver ) => {
const { fulfill: resolverFulfill = resolver } = resolver;
return { ...resolver, fulfill: resolverFulfill };
} );

return {
resolvers: mappedResolvers,
selectors: mapValues( selectors, mapSelector ),
};
}

/**
* Bundles up fulfillment functions for resolvers.
* @param {Object} registry Registry reference, for fulfilling via resolvers
* @param {string} key Part of the state shape to register the
* selectors for.
* @return {Object} An object providing fulfillment functions.
*/
function getCoreDataFulfillment( registry, key ) {
const { hasStartedResolution } = registry.select( 'core/data' );
const { startResolution, finishResolution } = registry.dispatch( 'core/data' );

return {
hasStarted: ( ...args ) => hasStartedResolution( key, ...args ),
start: ( ...args ) => startResolution( key, ...args ),
finish: ( ...args ) => finishResolution( key, ...args ),
fulfill: ( ...args ) => fulfillWithRegistry( registry, key, ...args ),
};
}

/**
* Calls a resolver given arguments
*
* @param {Object} registry Registry reference, for fulfilling via resolvers
* @param {string} key Part of the state shape to register the
* selectors for.
* @param {Object} store Store reference, for fulfilling via resolvers
* @param {Object} resolvers Store Resolvers
* @param {string} selectorName Selector name to fulfill.
* @param {Array} args Selector Arguments.
* @param {Array} args Selector Arguments.
*/
async function fulfillWithRegistry( registry, key, selectorName, ...args ) {
const namespace = registry.stores[ key ];
const resolver = get( namespace, [ 'resolvers', selectorName ] );
async function fulfillResolver( store, resolvers, selectorName, ...args ) {
const resolver = get( resolvers, [ selectorName ] );
if ( ! resolver ) {
return;
}

const action = resolver.fulfill( ...args );
if ( action ) {
await namespace.store.dispatch( action );
await store.dispatch( action );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
* Returns an action object used in signalling that selector resolution has
* started.
*
* @param {string} reducerKey Registered store reducer key.
* @param {string} selectorName Name of selector for which resolver triggered.
* @param {...*} args Arguments to associate for uniqueness.
*
* @return {Object} Action object.
*/
export function startResolution( reducerKey, selectorName, args ) {
export function startResolution( selectorName, args ) {
return {
type: 'START_RESOLUTION',
reducerKey,
selectorName,
args,
};
Expand All @@ -21,16 +19,14 @@ export function startResolution( reducerKey, selectorName, args ) {
* Returns an action object used in signalling that selector resolution has
* completed.
*
* @param {string} reducerKey Registered store reducer key.
* @param {string} selectorName Name of selector for which resolver triggered.
* @param {...*} args Arguments to associate for uniqueness.
*
* @return {Object} Action object.
*/
export function finishResolution( reducerKey, selectorName, args ) {
export function finishResolution( selectorName, args ) {
return {
type: 'FINISH_RESOLUTION',
reducerKey,
selectorName,
args,
};
Expand All @@ -39,53 +35,43 @@ export function finishResolution( reducerKey, selectorName, args ) {
/**
* Returns an action object used in signalling that we should invalidate the resolution cache.
*
* @param {string} reducerKey Registered store reducer key.
* @param {string} selectorName Name of selector for which resolver should be invalidated.
* @param {Array} args Arguments to associate for uniqueness.
*
* @return {Object} Action object.
*/
export function invalidateResolution( reducerKey, selectorName, args ) {
export function invalidateResolution( selectorName, args ) {
return {
type: 'INVALIDATE_RESOLUTION',
reducerKey,
selectorName,
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.
* Returns an action object used in signalling that the resolution
* should be invalidated.
*
* @return {Object} Action object.
*/
export function invalidateResolutionForStore( reducerKey ) {
export function invalidateResolutionForStore() {
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.
* given 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
) {
export function invalidateResolutionForStoreSelector( selectorName ) {
return {
type: 'INVALIDATE_RESOLUTION_FOR_STORE_SELECTOR',
reducerKey,
selectorName,
};
}
Loading

0 comments on commit 1e8d781

Please sign in to comment.