-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor the core/data store to be independent from the registry #14634
Changes from 1 commit
9044dc5
a215c0e
b440ce8
43b95fb
59d43d2
7400e01
7e6e3af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -27,16 +36,17 @@ 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 ) => ( 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; | ||
} | ||
|
@@ -84,15 +94,32 @@ 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, initialState, flowRight( enhancers ) ); | ||
} | ||
|
||
/** | ||
|
@@ -151,11 +178,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 }; | ||
} ); | ||
youknowriad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const mapSelector = ( selector, selectorName ) => { | ||
const resolver = resolvers[ selectorName ]; | ||
if ( ! resolver ) { | ||
|
@@ -169,68 +200,42 @@ function mapResolvers( resolvers, selectors, fulfillment, store ) { | |
return; | ||
} | ||
|
||
if ( fulfillment.hasStarted( selectorName, args ) ) { | ||
if ( metadataSelectors.hasStartedResolution( store.getState().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 ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I seem to be running into an issue where this doesn't actually wait for the fulfillment to complete before marking the resolution as finished: (Note in last three actions, I'd have expected "RECEIVE_ITEMS" to have occurred prior to "FINISH_RESOLUTION") I tried to consider how to reproduce this outside my branch, but since we wait for most all data to be available before rendering anything, it's hard to recreate even when adding a filter like I'll try to clean up my local working branch to push later this afternoon, but raising now in case anything comes to mind. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found the issue. I'll have a pull request shortly. Edit: #14711 |
||
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 ); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An initial thought occurred to me that there's risk for conflict between these metadata actions and a store's own. On further consideration, I don't see it likely to occur, or at least it should be known that these "baseline" actions/selectors exist for every store. Which leads me to wonder: How do we document this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I assume these can't be auto-documented. I'll see if I can add a note in the data module README (where we document
registerStore
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not so overly concerned about the selectors themselves (though it would be nice), more to just general awareness that this is a behavior to expect, the fact that resolution behavior is included in every store by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's potential side-effects here of consuming code being able to "override" default metadata selectors and actions. I almost wonder if there should be some enforcement here: i.e. checking the incoming actions/selectors and ensuring there are no collisions with the corresponding metadata functions? (or maybe change the order in the constructed array so metadata always overwrites the stores own?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're not confident exposing these selectors and actions yet, we can easily add
__internal
or__experimental
for now, what do you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm less concerned with exposing them (I think they should be exposed?) and more concerned with potential conflicts.
I do think it'd be better (at least initially) to do this:
That way the metadataActions and metadataSelectors take precedence. Of course you may intentionally have them this way so they can be overridden by the incoming store config but if not, I think this is safer.