Skip to content

Commit

Permalink
Data: Improve yielding flexibility by normalizing to async iterator
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Mar 20, 2018
1 parent 0032097 commit f1261b6
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 4 deletions.
52 changes: 48 additions & 4 deletions data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,14 @@ export function registerResolvers( reducerKey, newResolvers ) {

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

// Returning a promise from resolver dispatches upon resolution.
if ( fulfillment instanceof Promise ) {
fulfillment = [ fulfillment ];
} else if ( ! isAsyncIterable( fulfillment ) ) {
// 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 );
}
Expand Down Expand Up @@ -381,6 +381,50 @@ export function isAsyncIterable( object ) {
);
}

/**
* 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 const query = ( mapSelectToProps ) => {
deprecated( 'wp.data.query', {
version: '2.5',
Expand Down
146 changes: 146 additions & 0 deletions data/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
subscribe,
isActionLike,
isAsyncIterable,
isIterable,
toAsyncIterable,
} from '../';

jest.mock( '@wordpress/utils', () => ( {
Expand Down Expand Up @@ -121,6 +123,77 @@ describe( 'registerResolvers', () => {
expect( resolver ).toHaveBeenCalledTimes( 2 );
} );

it( 'should resolve action to dispatch', ( done ) => {
registerReducer( 'demo', ( state = 'NOTOK', action ) => {
return action.type === 'SET_OK' ? 'OK' : state;
} );
registerSelectors( 'demo', {
getValue: ( state ) => state,
} );
registerResolvers( 'demo', {
getValue: () => ( { type: 'SET_OK' } ),
} );

subscribeWithUnsubscribe( () => {
try {
expect( select( 'demo' ).getValue() ).toBe( 'OK' );
done();
} catch ( error ) {
done( error );
}
} );

select( 'demo' ).getValue();
} );

it( 'should resolve mixed type action array to dispatch', ( done ) => {
registerReducer( 'counter', ( state = 0, action ) => {
return action.type === 'INCREMENT' ? state + 1 : state;
} );
registerSelectors( 'counter', {
getCount: ( state ) => state,
} );
registerResolvers( 'counter', {
getCount: () => [
{ type: 'INCREMENT' },
Promise.resolve( { type: 'INCREMENT' } ),
],
} );

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

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

it( 'should resolve generator action to dispatch', ( done ) => {
registerReducer( 'demo', ( state = 'NOTOK', action ) => {
return action.type === 'SET_OK' ? 'OK' : state;
} );
registerSelectors( 'demo', {
getValue: ( state ) => state,
} );
registerResolvers( 'demo', {
* getValue() {
yield { type: 'SET_OK' };
},
} );

subscribeWithUnsubscribe( () => {
try {
expect( select( 'demo' ).getValue() ).toBe( 'OK' );
done();
} catch ( error ) {
done( error );
}
} );

select( 'demo' ).getValue();
} );

it( 'should resolve promise action to dispatch', ( done ) => {
registerReducer( 'demo', ( state = 'NOTOK', action ) => {
return action.type === 'SET_OK' ? 'OK' : state;
Expand Down Expand Up @@ -633,3 +706,76 @@ describe( 'isAsyncIterable', () => {
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 } );
} );
} );

0 comments on commit f1261b6

Please sign in to comment.