Skip to content

Commit

Permalink
Data Module: Add support for action creators as generators
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Jun 6, 2018
1 parent 4ec4177 commit fae11a7
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 202 deletions.
97 changes: 10 additions & 87 deletions packages/data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import isShallowEqual from '@wordpress/is-shallow-equal';
* Internal dependencies
*/
import registerDataStore from './store';
import createStoreRuntime from './runtime';

export { loadAndPersist, withRehydration, withRehydratation } from './persist';

/**
* Module constants
*/
const stores = {};
const runtimes = {};
const selectors = {};
const actions = {};
let listeners = [];
Expand Down Expand Up @@ -97,6 +99,9 @@ export function registerReducer( reducerKey, reducer ) {
}
} );

// Create the actions runtime
runtimes[ reducerKey ] = createStoreRuntime( store );

return store;
}

Expand Down Expand Up @@ -147,6 +152,7 @@ export function registerResolvers( reducerKey, newResolvers ) {
}

const store = stores[ reducerKey ];
const runtime = runtimes[ reducerKey ];

// Normalize resolver shape to object.
let resolver = newResolvers[ selectorName ];
Expand All @@ -165,20 +171,9 @@ export function registerResolvers( reducerKey, newResolvers ) {
// state, it would not be otherwise provided to fulfill.
const state = store.getState();

let fulfillment = resolver.fulfill( state, ...args );
const 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 );
}
}
await runtime( fulfillment );

finishResolution( reducerKey, selectorName, args );
}
Expand Down Expand Up @@ -212,8 +207,8 @@ export function registerResolvers( reducerKey, newResolvers ) {
* @param {Object} newActions Actions to register.
*/
export function registerActions( reducerKey, newActions ) {
const store = stores[ reducerKey ];
const createBoundAction = ( action ) => ( ...args ) => store.dispatch( action( ...args ) );
const runtime = runtimes[ reducerKey ];
const createBoundAction = ( action ) => ( ...args ) => runtime( action( ...args ) );
actions[ reducerKey ] = mapValues( newActions, createBoundAction );
}

Expand Down Expand Up @@ -402,76 +397,4 @@ export const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent
'withDispatch'
);

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

registerDataStore();
90 changes: 90 additions & 0 deletions packages/data/src/runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@

/**
* 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 default function createStoreRuntime( store ) {
return async ( actionCreator ) => {
if ( isActionLike( actionCreator ) ) {
store.dispatch( actionCreator );
return;
}

// Attempt to normalize the action creator as async iterable.
actionCreator = toAsyncIterable( actionCreator );
for await ( const maybeAction of actionCreator ) {
// Dispatch if it quacks like an action.
if ( isActionLike( maybeAction ) ) {
store.dispatch( maybeAction );
}
}
};
}
115 changes: 0 additions & 115 deletions packages/data/src/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ import {
withSelect,
withDispatch,
subscribe,
isActionLike,
isAsyncIterable,
isIterable,
toAsyncIterable,
} from '../';

// Mock data store to prevent self-initialization, as it needs to be reset
Expand Down Expand Up @@ -751,114 +747,3 @@ describe( 'dispatch', () => {
expect( store.getState() ).toBe( 5 );
} );
} );

describe( 'isActionLike', () => {
it( 'returns false if non-action-like', () => {
expect( isActionLike( undefined ) ).toBe( false );
expect( isActionLike( null ) ).toBe( false );
expect( isActionLike( [] ) ).toBe( false );
expect( isActionLike( {} ) ).toBe( false );
expect( isActionLike( 1 ) ).toBe( false );
expect( isActionLike( 0 ) ).toBe( false );
expect( isActionLike( Infinity ) ).toBe( false );
expect( isActionLike( { type: null } ) ).toBe( false );
} );

it( 'returns true if action-like', () => {
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;
} );
} );

describe( 'isIterable', () => {
it( 'returns false if not iterable', () => {
expect( isIterable( undefined ) ).toBe( false );
expect( isIterable( null ) ).toBe( false );
expect( isIterable( {} ) ).toBe( false );
expect( isIterable( Promise.resolve( {} ) ) ).toBe( false );
} );

it( 'returns true if iterable', () => {
function* getIterable() {
yield 'foo';
}

const result = getIterable();

expect( isIterable( result ) ).toBe( true );
expect( isIterable( [] ) ).toBe( true );
} );
} );

describe( 'toAsyncIterable', () => {
it( 'normalizes async iterable', async () => {
async function* getAsyncIterable() {
yield await Promise.resolve( { ok: true } );
}

const object = getAsyncIterable();
const normalized = toAsyncIterable( object );

expect( ( await normalized.next() ).value ).toEqual( { ok: true } );
} );

it( 'normalizes promise', async () => {
const object = Promise.resolve( { ok: true } );
const normalized = toAsyncIterable( object );

expect( ( await normalized.next() ).value ).toEqual( { ok: true } );
} );

it( 'normalizes object', async () => {
const object = { ok: true };
const normalized = toAsyncIterable( object );

expect( ( await normalized.next() ).value ).toEqual( { ok: true } );
} );

it( 'normalizes array of promise', async () => {
const object = [ Promise.resolve( { ok: true } ) ];
const normalized = toAsyncIterable( object );

expect( ( await normalized.next() ).value ).toEqual( { ok: true } );
} );

it( 'normalizes mixed array', async () => {
const object = [ { foo: 'bar' }, Promise.resolve( { ok: true } ) ];
const normalized = toAsyncIterable( object );

expect( ( await normalized.next() ).value ).toEqual( { foo: 'bar' } );
expect( ( await normalized.next() ).value ).toEqual( { ok: true } );
} );

it( 'normalizes generator', async () => {
function* getIterable() {
yield Promise.resolve( { ok: true } );
}

const object = getIterable();
const normalized = toAsyncIterable( object );

expect( ( await normalized.next() ).value ).toEqual( { ok: true } );
} );
} );
Loading

0 comments on commit fae11a7

Please sign in to comment.