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

Data Module: Add support for action creators as generators #6999

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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