Skip to content

Commit

Permalink
Data: Support async generator resolver functions
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Mar 16, 2018
1 parent 1948430 commit ffb0760
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 36 deletions.
10 changes: 6 additions & 4 deletions blocks/library/categories/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,8 @@ class CategoriesBlock extends Component {
}

render() {
const { attributes, focus, setAttributes } = this.props;
const { attributes, focus, setAttributes, isRequesting } = this.props;
const { align, displayAsDropdown, showHierarchy, showPostCounts } = attributes;
const categories = this.getCategories();

const inspectorControls = focus && (
<InspectorControls key="inspector">
Expand All @@ -168,7 +167,7 @@ class CategoriesBlock extends Component {
</InspectorControls>
);

if ( ! categories.length ) {
if ( isRequesting ) {
return [
inspectorControls,
<Placeholder
Expand Down Expand Up @@ -206,7 +205,10 @@ class CategoriesBlock extends Component {
}

export default withSelect( ( select ) => {
const { getCategories, isRequestingCategories } = select( 'core' );

return {
categories: select( 'core' ).getCategories(),
categories: getCategories(),
isRequesting: isRequestingCategories(),
};
} )( CategoriesBlock );
15 changes: 15 additions & 0 deletions core-data/actions.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/**
* Returns an action object used in signalling that the request for a given
* data type has been made.
*
* @param {string} dataType Data type requested.
*
* @return {Object} Action object.
*/
export function setRequested( dataType ) {
return {
type: 'SET_REQUESTED',
dataType,
};
}

/**
* Returns an action object used in signalling that categories have been
* received.
Expand Down
41 changes: 29 additions & 12 deletions core-data/reducer.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,47 @@
/**
* External dependencies
*/
import { uniqBy } from 'lodash';
import { combineReducers } from 'redux';

/**
* Reducer returning the categories list.
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
*
* @return {string} Updated state.
*/
function categories( state = [], action ) {
function categories( state = null, action ) {
switch ( action.type ) {
case 'RECEIVE_CATEGORIES':
return uniqBy(
[
...action.categories,
...state,
],
( category ) => category.id
);
return [ ...action.categories ];
}

return state;
}

export default combineReducers( { categories } );
/**
* Reducer returning requested state, tracking whether requests have been
* issued for a given data type.
*
* @param {Object} state Current state.
* @param {Object} action Action object.
*
* @return {Object} Next state.
*/
function requested( state = {}, action ) {
switch ( action.type ) {
case 'SET_REQUESTED':
return {
...state,
[ action.dataType ]: true,
};
}

return state;
}

export default combineReducers( {
categories,
requested,
} );
13 changes: 6 additions & 7 deletions core-data/resolvers.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
/**
* Internal dependencies
*/
import { receiveCategories } from './actions';
import { setRequested, receiveCategories } from './actions';

/**
* Requests categories from the REST API, returning a promise resolving to an
* action object for receiving categories.
*
* @return {Promise<Object>} Categories request promise.
* Requests categories from the REST API, yielding action objects on request
* progress.
*/
export async function getCategories() {
export async function* getCategories() {
yield setRequested( 'categories' );
const categories = await wp.apiRequest( { path: '/wp/v2/categories' } );
return receiveCategories( categories );
yield receiveCategories( categories );
}
33 changes: 30 additions & 3 deletions core-data/selectors.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@

/**
* Returns all the available categories.
*
* @param {Object} state Global application state.
* @param {Object} state Data state.
*
* @return {Array} Categories List
* @return {Array} Categories list.
*/
export function getCategories( state ) {
return state.categories;
}

/**
* Returns true if a request has been issued for the given data type, or false
* otherwise.
*
* @param {Object} state Data state.
* @param {string} dataType Data type to test.
*
* @return {boolean} Whether data type has been requested.
*/
export function hasRequested( state, dataType ) {
return !! state.requested[ dataType ];
}

/**
* Returns true if a request is in progress for categories data, or false
* otherwise.
*
* @param {Object} state Data state.
*
* @return {boolean} Whether a request is in progress for categories.
*/
export function isRequestingCategories( state ) {
return (
hasRequested( state, 'categories' ) &&
getCategories( state ) === null
);
}
39 changes: 32 additions & 7 deletions data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ const selectors = {};
const actions = {};
let listeners = [];

// Babel transform doesn't provide its own `asyncIterator` symbol, but it does
// test for its presence at runtime, so define a shim value if not otherwise
// defined by the browser.
if ( typeof Symbol.asyncIterator !== 'symbol' ) {
Symbol.asyncIterator = Symbol( 'Symbol.asyncIterator' );
}

/**
* Global listener called for each store's update.
*/
Expand Down Expand Up @@ -138,22 +145,26 @@ export function registerResolvers( reducerKey, newResolvers ) {
}

// Ensure single invocation per argument set via memoization.
const fulfill = memoize( ( ...args ) => {
const fulfill = memoize( async ( ...args ) => {
const store = stores[ reducerKey ];

// At this point, selectors have already been pre-bound to inject
// state, it would not be otherwise provided to fulfill.
const state = store.getState();

const fulfillment = newResolvers[ key ]( state, ...args );
let fulfillment = newResolvers[ key ]( state, ...args );

// Returning a promise from resolver dispatches upon resolution.
if ( fulfillment instanceof Promise ) {
fulfillment.then( ( action ) => {
if ( isActionLike( action ) ) {
store.dispatch( action );
}
} );
fulfillment = [ fulfillment ];
} else if ( ! isAsyncIterable( fulfillment ) ) {
return;
}

for await ( const maybeAction of fulfillment ) {
if ( isActionLike( maybeAction ) ) {
store.dispatch( maybeAction );
}
}
} );

Expand Down Expand Up @@ -363,6 +374,20 @@ export function isActionLike( action ) {
);
}

/**
* Returns true if the given object is an async iterable, or false otherwise.
*
* @param {*} object Object to test.
*
* @return {boolean} Whether object is an async iterable.
*/
export function isAsyncIterable( object ) {
return (
!! object &&
typeof object[ Symbol.asyncIterator ] === 'function'
);
}

export const query = ( mapSelectToProps ) => {
deprecated( 'wp.data.query', {
version: '2.5',
Expand Down
45 changes: 45 additions & 0 deletions data/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
withDispatch,
subscribe,
isActionLike,
isAsyncIterable,
} from '../';

jest.mock( '@wordpress/utils', () => ( {
Expand Down Expand Up @@ -167,6 +168,29 @@ describe( 'registerResolvers', () => {
} );
} );

it( 'should resolve async iterator action to dispatch', ( done ) => {
registerReducer( 'counter', ( state = 0, action ) => {
return action.type === 'INCREMENT' ? state + 1 : state;
} );
registerSelectors( 'counter', {
getCount: ( state ) => state,
} );
registerResolvers( 'counter', {
getCount: async function* () {
yield { type: 'INCREMENT' };
yield await Promise.resolve( { type: 'INCREMENT' } );
},
} );

subscribeWithUnsubscribe( () => {
if ( select( 'counter' ).getCount() === 2 ) {
done();
}
} );

select( 'counter' ).getCount();
} );

it( 'should not dispatch resolved promise action on subsequent selector calls', ( done ) => {
registerReducer( 'demo', ( state = 'NOTOK', action ) => {
return action.type === 'SET_OK' && state === 'NOTOK' ? 'OK' : 'NOTOK';
Expand Down Expand Up @@ -588,3 +612,24 @@ describe( 'isActionLike', () => {
expect( isActionLike( { type: 'POW' } ) ).toBe( true );
} );
} );

describe( 'isAsyncIterable', () => {
it( 'returns false if not async iterable', () => {
expect( isAsyncIterable( undefined ) ).toBe( false );
expect( isAsyncIterable( null ) ).toBe( false );
expect( isAsyncIterable( [] ) ).toBe( false );
expect( isAsyncIterable( {} ) ).toBe( false );
} );

it( 'returns true if async iterable', async () => {
async function* getAsyncIterable() {
yield new Promise( ( resolve ) => process.nextTick( resolve ) );
}

const result = getAsyncIterable();

expect( isAsyncIterable( result ) ).toBe( true );

await result;
} );
} );
7 changes: 6 additions & 1 deletion eslint/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,17 @@ module.exports = {
semi: 'error',
'semi-spacing': 'error',
'space-before-blocks': [ 'error', 'always' ],
'space-before-function-paren': [ 'error', 'never' ],
'space-before-function-paren': [ 'error', {
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
} ],
'space-in-parens': [ 'error', 'always' ],
'space-infix-ops': [ 'error', { int32Hint: false } ],
'space-unary-ops': [ 'error', {
overrides: {
'!': true,
yield: true,
},
} ],
'template-curly-spacing': [ 'error', 'always' ],
Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

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

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"babel-core": "6.26.0",
"babel-eslint": "8.0.3",
"babel-loader": "7.1.2",
"babel-plugin-transform-async-generator-functions": "6.24.1",
"babel-traverse": "6.26.0",
"check-node-version": "3.1.1",
"codecov": "3.0.0",
Expand Down Expand Up @@ -98,6 +99,9 @@
"presets": [
"@wordpress/default"
],
"plugins": [
"transform-async-generator-functions"
],
"env": {
"production": {
"plugins": [
Expand All @@ -106,7 +110,8 @@
{
"output": "languages/gutenberg.pot"
}
]
],
"transform-async-generator-functions"
]
}
}
Expand All @@ -121,7 +126,8 @@
"preset": "@wordpress/jest-preset-default",
"setupFiles": [
"<rootDir>/test/unit/setup-blocks.js",
"<rootDir>/test/unit/setup-wp-aliases.js"
"<rootDir>/test/unit/setup-wp-aliases.js",
"<rootDir>/test/unit/shim-async-iterator-symbol.js"
],
"transform": {
"\\.pegjs$": "<rootDir>/test/unit/pegjs-transform.js"
Expand Down
3 changes: 3 additions & 0 deletions test/unit/shim-async-iterator-symbol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if ( typeof Symbol.asyncIterator !== 'symbol' ) {
Symbol.asyncIterator = Symbol( 'Symbol.asyncIterator' );
}

0 comments on commit ffb0760

Please sign in to comment.