Skip to content

Commit

Permalink
Data Module: Add sync generators support for actions and resolvers
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Jun 26, 2018
1 parent 27902d7 commit 6eb675c
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 303 deletions.
5 changes: 5 additions & 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"redux-optimist": "1.0.0",
"refx": "3.0.0",
"rememo": "3.0.0",
"rungen": "0.3.2",
"showdown": "1.8.6",
"simple-html-tokenizer": "0.4.1",
"tinycolor2": "1.4.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/core-data/src/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const getMethodName = ( kind, name, prefix = 'get', usePlural = false ) =
*
* @return {Array} Entities
*/
export async function* getKindEntities( state, kind ) {
export function* getKindEntities( state, kind ) {
let entities = getEntitiesByKind( state, kind );

if ( entities && entities.length !== 0 ) {
Expand All @@ -78,7 +78,7 @@ export async function* getKindEntities( state, kind ) {
return [];
}

entities = await kindConfig.loadEntities();
entities = yield kindConfig.loadEntities();
yield addEntities( entities );

return entities;
Expand Down
24 changes: 12 additions & 12 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ import { getKindEntities } from './entities';
* Requests categories from the REST API, yielding action objects on request
* progress.
*/
export async function* getCategories() {
const categories = await apiRequest( { path: '/wp/v2/categories?per_page=-1' } );
export function* getCategories() {
const categories = yield apiRequest( { path: '/wp/v2/categories?per_page=-1' } );
yield receiveTerms( 'categories', categories );
}

/**
* Requests authors from the REST API.
*/
export async function* getAuthors() {
const users = await apiRequest( { path: '/wp/v2/users/?who=authors&per_page=-1' } );
export function* getAuthors() {
const users = yield apiRequest( { path: '/wp/v2/users/?who=authors&per_page=-1' } );
yield receiveUserQuery( 'authors', users );
}

Expand All @@ -44,13 +44,13 @@ export async function* getAuthors() {
* @param {string} name Entity name.
* @param {number} key Record's key
*/
export async function* getEntityRecord( state, kind, name, key ) {
const entities = yield* await getKindEntities( state, kind );
export function* getEntityRecord( state, kind, name, key ) {
const entities = yield getKindEntities( state, kind );
const entity = find( entities, { kind, name } );
if ( ! entity ) {
return;
}
const record = await apiRequest( { path: `${ entity.baseUrl }/${ key }?context=edit` } );
const record = yield apiRequest( { path: `${ entity.baseUrl }/${ key }?context=edit` } );
yield receiveEntityRecords( kind, name, record );
}

Expand All @@ -61,20 +61,20 @@ export async function* getEntityRecord( state, kind, name, key ) {
* @param {string} kind Entity kind.
* @param {string} name Entity name.
*/
export async function* getEntityRecords( state, kind, name ) {
const entities = yield* await getKindEntities( state, kind );
export function* getEntityRecords( state, kind, name ) {
const entities = yield getKindEntities( state, kind );
const entity = find( entities, { kind, name } );
if ( ! entity ) {
return;
}
const records = await apiRequest( { path: `${ entity.baseUrl }?context=edit` } );
const records = yield apiRequest( { path: `${ entity.baseUrl }?context=edit` } );
yield receiveEntityRecords( kind, name, Object.values( records ) );
}

/**
* Requests theme supports data from the index.
*/
export async function* getThemeSupports() {
const index = await apiRequest( { path: '/' } );
export function* getThemeSupports() {
const index = yield apiRequest( { path: '/' } );
yield receiveThemeSupportsFromIndex( index );
}
11 changes: 6 additions & 5 deletions packages/core-data/src/test/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,27 @@ describe( 'getKindEntities', () => {
} );
} );

it( 'shouldn\'t do anything if the entities have already been resolved', async () => {
it( 'shouldn\'t do anything if the entities have already been resolved', () => {
const state = {
entities: { config: [ { kind: 'postType' } ] },
};
const fulfillment = getKindEntities( state, 'postType' );
const done = ( await fulfillment.next() ).done;
const done = fulfillment.next().done;
expect( done ).toBe( true );
} );

it( 'shouldn\'t do anything if there no defined kind config', async () => {
it( 'shouldn\'t do anything if there no defined kind config', () => {
const state = { entities: { config: [] } };
const fulfillment = getKindEntities( state, 'unknownKind' );
const done = ( await fulfillment.next() ).done;
const done = fulfillment.next().done;
expect( done ).toBe( true );
} );

it( 'should fetch and add the entities', async () => {
const state = { entities: { config: [] } };
const fulfillment = getKindEntities( state, 'postType' );
const received = ( await fulfillment.next() ).value;
const receivedEntities = await fulfillment.next().value;
const received = ( fulfillment.next( receivedEntities ) ).value;
expect( received ).toEqual( addEntities( [ {
baseUrl: '/wp/v2/posts',
kind: 'postType',
Expand Down
47 changes: 10 additions & 37 deletions packages/core-data/src/test/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ describe( 'getCategories', () => {

it( 'yields with requested terms', async () => {
const fulfillment = getCategories();
const received = ( await fulfillment.next() ).value;
const categories = await fulfillment.next().value;
const received = fulfillment.next( categories ).value;
expect( received ).toEqual( receiveTerms( 'categories', CATEGORIES ) );
} );
} );
Expand All @@ -43,39 +44,16 @@ describe( 'getEntityRecord', () => {
if ( options.path === '/wp/v2/types/post?context=edit' ) {
return Promise.resolve( POST_TYPE );
}
if ( options.path === '/wp/v2/posts/10?context=edit' ) {
return Promise.resolve( POST );
}
if ( options.path === '/wp/v2/types?context=edit' ) {
return Promise.resolve( POST_TYPES );
}
} );
} );

it( 'yields with requested post type', async () => {
const state = {
entities: {
config: [
{ name: 'postType', kind: 'root', baseUrl: '/wp/v2/types' },
],
},
};
const fulfillment = getEntityRecord( state, 'root', 'postType', 'post' );
const received = ( await fulfillment.next() ).value;
const fulfillment = getEntityRecord( {}, 'root', 'postType', 'post' );
fulfillment.next(); // Trigger the getKindEntities generator
const records = await fulfillment.next( [ { name: 'postType', kind: 'root', baseUrl: '/wp/v2/types' } ] ).value;
const received = await fulfillment.next( records ).value;
expect( received ).toEqual( receiveEntityRecords( 'root', 'postType', POST_TYPE ) );
} );

it( 'loads the kind entities and yields with requested post type', async () => {
const fulfillment = getEntityRecord( { entities: {} }, 'postType', 'post', 10 );
const receivedEntities = ( await fulfillment.next() ).value;
expect( receivedEntities ).toEqual( addEntities( [ {
baseUrl: '/wp/v2/posts',
kind: 'postType',
name: 'post',
} ] ) );
const received = ( await fulfillment.next() ).value;
expect( received ).toEqual( receiveEntityRecords( 'postType', 'post', POST ) );
} );
} );

describe( 'getEntityRecords', () => {
Expand All @@ -93,15 +71,10 @@ describe( 'getEntityRecords', () => {
} );

it( 'yields with requested post type', async () => {
const state = {
entities: {
config: [
{ name: 'postType', kind: 'root', baseUrl: '/wp/v2/types' },
],
},
};
const fulfillment = getEntityRecords( state, 'root', 'postType' );
const received = ( await fulfillment.next() ).value;
const fulfillment = getEntityRecords( {}, 'root', 'postType' );
fulfillment.next(); // Trigger the getKindEntities generator
const records = await fulfillment.next( [ { name: 'postType', kind: 'root', baseUrl: '/wp/v2/types' } ] ).value;
const received = ( await fulfillment.next( records ) ).value;
expect( received ).toEqual( receiveEntityRecords( 'root', 'postType', Object.values( POST_TYPES ) ) );
} );
} );
109 changes: 15 additions & 94 deletions packages/data/src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,13 @@
*/
import { createStore } from 'redux';
import { flowRight, without, mapValues, overEvery, get } from 'lodash';
import createStoreRuntime from './runtime';

/**
* Internal dependencies
*/
import dataStore from './store';

/**
* Returns true if the given argument appears to be a dispatchable action.
*
* @param {*} action Object to test.
*
* @return {boolean} Whether object is action-like.
*/
export function isActionLike( action ) {
return (
!! action &&
typeof action.type === 'string'
);
}

/**
* 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'
);
}

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

/**
* Normalizes the given object argument to an async iterable, asynchronously
* yielding on a singular or array of generator yields or promise resolution.
*
* @param {*} object Object to normalize.
*
* @return {AsyncGenerator} Async iterable actions.
*/
export function toAsyncIterable( object ) {
if ( isAsyncIterable( object ) ) {
return object;
}

return ( async function* () {
// Normalize as iterable...
if ( ! isIterable( object ) ) {
object = [ object ];
}

for ( let maybeAction of object ) {
// ...of Promises.
if ( ! ( maybeAction instanceof Promise ) ) {
maybeAction = Promise.resolve( maybeAction );
}

yield await maybeAction;
}
}() );
}

export function createRegistry( storeConfigs = {} ) {
const namespaces = {};
let listeners = [];
Expand All @@ -106,7 +35,10 @@ export function createRegistry( storeConfigs = {} ) {
enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__( { name: reducerKey, instanceId: reducerKey } ) );
}
const store = createStore( reducer, flowRight( enhancers ) );
namespaces[ reducerKey ] = { store };
namespaces[ reducerKey ] = {
runtime: createStoreRuntime( store ),
store,
};

// Customize subscribe behavior to call listeners only on effective change,
// not on every dispatch.
Expand Down Expand Up @@ -159,14 +91,15 @@ export function createRegistry( storeConfigs = {} ) {
}

const store = namespaces[ reducerKey ].store;
const runtime = namespaces[ reducerKey ].runtime;

// Normalize resolver shape to object.
let resolver = newResolvers[ selectorName ];
if ( ! resolver.fulfill ) {
resolver = { fulfill: resolver };
}

async function fulfill( ...args ) {
function fulfill( ...args ) {
if ( hasStartedResolution( reducerKey, selectorName, args ) ) {
return;
}
Expand All @@ -177,27 +110,15 @@ export function createRegistry( storeConfigs = {} ) {
// state, it would not be otherwise provided to fulfill.
const state = store.getState();

let fulfillment = resolver.fulfill( state, ...args );

// Attempt to normalize fulfillment as async iterable.
fulfillment = toAsyncIterable( fulfillment );
if ( ! isAsyncIterable( fulfillment ) ) {
return;
}

for await ( const maybeAction of fulfillment ) {
// Dispatch if it quacks like an action.
if ( isActionLike( maybeAction ) ) {
store.dispatch( maybeAction );
}
}

finishResolution( reducerKey, selectorName, args );
const fulfillment = resolver.fulfill( state, ...args );
runtime( fulfillment, () => {
finishResolution( reducerKey, selectorName, args );
} );
}

if ( typeof resolver.isFulfilled === 'function' ) {
// When resolver provides its own fulfillment condition, fulfill
// should only occur if not already fulfilled (opt-out condition).
// When resolver provides its own fulfillment condition, fulfill
// should only occur if not already fulfilled (opt-out condition).
fulfill = overEvery( [
( ...args ) => {
const state = store.getState();
Expand All @@ -224,8 +145,8 @@ export function createRegistry( storeConfigs = {} ) {
* @param {Object} newActions Actions to register.
*/
function registerActions( reducerKey, newActions ) {
const store = namespaces[ reducerKey ].store;
const createBoundAction = ( action ) => ( ...args ) => store.dispatch( action( ...args ) );
const runtime = namespaces[ reducerKey ].runtime;
const createBoundAction = ( action ) => ( ...args ) => runtime( action( ...args ) );
namespaces[ reducerKey ].actions = mapValues( newActions, createBoundAction );
}

Expand Down
Loading

0 comments on commit 6eb675c

Please sign in to comment.