Skip to content
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

Support private selectors and actions for stores registered via registry.registerStore() and for sub registries. #47421

Merged
merged 7 commits into from
Jan 26, 2023
25 changes: 17 additions & 8 deletions packages/data/src/redux-store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ function createResolversCache() {
export default function createReduxStore( key, options ) {
const privateActions = {};
const privateSelectors = {};
const privateRegistrationFunctions = {
privateActions,
registerPrivateActions: ( actions ) => {
Object.assign( privateActions, actions );
},
privateSelectors,
registerPrivateSelectors: ( selectors ) => {
Object.assign( privateSelectors, selectors );
},
};
const storeDescriptor = {
name: key,
instantiate: ( registry ) => {
Expand Down Expand Up @@ -141,6 +151,9 @@ export default function createReduxStore( key, options ) {
registry,
thunkArgs
);
// Expose the private registration functions on the store
// so they can be copied to a sub registry in registry.js.
lock( store, privateRegistrationFunctions );
const resolversCache = createResolversCache();

let resolvers;
Expand Down Expand Up @@ -260,14 +273,10 @@ export default function createReduxStore( key, options ) {
},
};

lock( storeDescriptor, {
registerPrivateActions: ( actions ) => {
Object.assign( privateActions, actions );
},
registerPrivateSelectors: ( selectors ) => {
Object.assign( privateSelectors, selectors );
},
} );
// Expose the private registration functions on the store
// descriptor. That's a natural choice since that's where the
// public actions and selectors are stored .
lock( storeDescriptor, privateRegistrationFunctions );

return storeDescriptor;
}
Expand Down
38 changes: 37 additions & 1 deletion packages/data/src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import deprecated from '@wordpress/deprecated';
import createReduxStore from './redux-store';
import coreDataStore from './store';
import { createEmitter } from './utils/emitter';
import { lock, unlock } from './experiments';

/** @typedef {import('./types').StoreDescriptor} StoreDescriptor */

Expand Down Expand Up @@ -245,6 +246,22 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
};
stores[ name ] = store;
store.subscribe( globalListener );

// Copy private actions and selectors from the parent store.
if ( parent ) {
try {
unlock( store.store ).registerPrivateActions(
unlock( parent ).privateActionsOf( name )
);
unlock( store.store ).registerPrivateSelectors(
unlock( parent ).privateSelectorsOf( name )
);
} catch ( e ) {
// unlock() throws if store.store was not locked.
// The error indicates there's nothing to do here so let's
// ignore it.
}
}
}

/**
Expand Down Expand Up @@ -334,5 +351,24 @@ export function createRegistry( storeConfigs = {}, parent = null ) {
parent.subscribe( globalListener );
}

return withPlugins( registry );
const registryWithPlugins = withPlugins( registry );
lock( registryWithPlugins, {
privateActionsOf: ( name ) => {
try {
return unlock( stores[ name ].store ).privateActions;
} catch ( e ) {
// unlock() throws an error the store was not locked – this means
// there no private actions are available
return {};
}
},
privateSelectorsOf: ( name ) => {
try {
return unlock( stores[ name ].store ).privateSelectors;
} catch ( e ) {
return {};
}
},
} );
return registryWithPlugins;
}
104 changes: 80 additions & 24 deletions packages/data/src/test/privateAPIs.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,34 @@ describe( 'Private data APIs', () => {
function setPublicPrice( price ) {
return { type: 'SET_PUBLIC_PRICE', price };
}
function createStore() {
const groceryStore = createReduxStore( 'grocer', {
selectors: {
getPublicPrice,
getState: ( state ) => state,
},
actions: { setPublicPrice },
reducer: ( state, action ) => {
if ( action?.type === 'SET_PRIVATE_PRICE' ) {
return {
...state,
secretDiscount: action?.price,
};
} else if ( action?.type === 'SET_PUBLIC_PRICE' ) {
return {
...state,
price: action?.price,
};
}
const storeName = 'grocer';
const storeDescriptor = {
selectors: {
getPublicPrice,
getState: ( state ) => state,
},
actions: { setPublicPrice },
reducer: ( state, action ) => {
if ( action?.type === 'SET_PRIVATE_PRICE' ) {
return {
price: 1000,
secretDiscount: 800,
...( state || {} ),
...state,
secretDiscount: action?.price,
};
},
} );
} else if ( action?.type === 'SET_PUBLIC_PRICE' ) {
return {
...state,
price: action?.price,
};
}
return {
price: 1000,
secretDiscount: 800,
...( state || {} ),
};
},
};
function createStore() {
const groceryStore = createReduxStore( storeName, storeDescriptor );
registry.register( groceryStore );
return groceryStore;
}
Expand Down Expand Up @@ -126,6 +128,28 @@ describe( 'Private data APIs', () => {
const unlockedSelectors = unlock( registry.select( groceryStore ) );
expect( unlockedSelectors.getPublicPrice() ).toEqual( 1000 );
} );

it( 'should support sub registries', () => {
const groceryStore = registry.registerStore(
storeName,
storeDescriptor
);
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const subRegistry = createRegistry( {}, registry );
subRegistry.registerStore( storeName, storeDescriptor );

const parentPrivateSelectors = unlock(
registry.select( storeName )
);
expect( parentPrivateSelectors.getSecretDiscount() ).toEqual( 800 );

const subPrivateSelectors = unlock(
subRegistry.select( storeName )
);
expect( subPrivateSelectors.getSecretDiscount() ).toEqual( 800 );
} );
} );

describe( 'private actions', () => {
Expand Down Expand Up @@ -224,5 +248,37 @@ describe( 'Private data APIs', () => {
unlock( registry.select( groceryStore ) ).getSecretDiscount()
).toEqual( 100 );
} );

it( 'should support sub registries', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const subRegistry = createRegistry( {}, registry );
subRegistry.registerStore( storeName, storeDescriptor );

const parentPrivateActions = unlock(
registry.dispatch( storeName )
);
const parentPrivateSelectors = unlock(
registry.select( storeName )
);

const subPrivateActions = unlock(
subRegistry.dispatch( storeName )
);
const subPrivateSelectors = unlock(
subRegistry.select( storeName )
);

parentPrivateActions.setSecretDiscount( 400 );
subPrivateActions.setSecretDiscount( 478 );

expect( parentPrivateSelectors.getSecretDiscount() ).toEqual( 400 );
expect( subPrivateSelectors.getSecretDiscount() ).toEqual( 478 );
} );
} );
} );