diff --git a/packages/data/src/index.js b/packages/data/src/index.js index 41cca7d50df14..1ba7c46c3aaf1 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -20,6 +20,7 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; * Internal dependencies */ import registerDataStore from './store'; +import createStoreRuntime from './runtime'; export { loadAndPersist, withRehydration, withRehydratation } from './persist'; @@ -27,6 +28,7 @@ export { loadAndPersist, withRehydration, withRehydratation } from './persist'; * Module constants */ const stores = {}; +const runtimes = {}; const selectors = {}; const actions = {}; let listeners = []; @@ -97,6 +99,9 @@ export function registerReducer( reducerKey, reducer ) { } } ); + // Create the actions runtime + runtimes[ reducerKey ] = createStoreRuntime( store ); + return store; } @@ -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 ]; @@ -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 ); } @@ -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 ); } @@ -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(); diff --git a/packages/data/src/runtime.js b/packages/data/src/runtime.js new file mode 100644 index 0000000000000..d5dc066d4939d --- /dev/null +++ b/packages/data/src/runtime.js @@ -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 ); + } + } + }; +} diff --git a/packages/data/src/test/index.js b/packages/data/src/test/index.js index 078810013d8dc..d0d5a624baec7 100644 --- a/packages/data/src/test/index.js +++ b/packages/data/src/test/index.js @@ -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 @@ -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 } ); - } ); -} ); diff --git a/packages/data/src/test/runtime.js b/packages/data/src/test/runtime.js new file mode 100644 index 0000000000000..7c6928f76c7dc --- /dev/null +++ b/packages/data/src/test/runtime.js @@ -0,0 +1,120 @@ +/** + * Internal dependencies + */ +import { + isActionLike, + isAsyncIterable, + isIterable, + toAsyncIterable, +} from '../runtime'; + +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 } ); + } ); +} );