From 7f1966d565a7ae085ea598e44df8e4fb65b9ecb9 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 15 Jan 2019 18:45:47 +0000 Subject: [PATCH] Async mode in the data module (#13056) --- docs/manifest.json | 6 ++ lib/packages-dependencies.php | 2 + package-lock.json | 7 +++ package.json | 1 + packages/data/package.json | 1 + .../components/async-mode-provider/index.js | 10 +++ .../data/src/components/with-select/index.js | 42 ++++++++++--- packages/data/src/index.js | 1 + .../plugins/meta-attribute-block.test.js | 11 +++- .../editor/src/components/block-list/index.js | 62 ++++++++++++++----- packages/priority-queue/.npmrc | 1 + packages/priority-queue/CHANGELOG.md | 3 + packages/priority-queue/README.md | 32 ++++++++++ packages/priority-queue/package.json | 29 +++++++++ packages/priority-queue/src/index.js | 50 +++++++++++++++ 15 files changed, 231 insertions(+), 27 deletions(-) create mode 100644 packages/data/src/components/async-mode-provider/index.js create mode 100644 packages/priority-queue/.npmrc create mode 100644 packages/priority-queue/CHANGELOG.md create mode 100644 packages/priority-queue/README.md create mode 100644 packages/priority-queue/package.json create mode 100644 packages/priority-queue/src/index.js diff --git a/docs/manifest.json b/docs/manifest.json index fb69aae37a09b..2958928a39777 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -731,6 +731,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/postcss-themes/README.md", "parent": "packages" }, + { + "title": "@wordpress/priority-queue", + "slug": "packages-priority-queue", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/priority-queue/README.md", + "parent": "packages" + }, { "title": "@wordpress/redux-routine", "slug": "packages-redux-routine", diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index 9db4e07187ac0..848b661b9c213 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -91,6 +91,7 @@ 'wp-compose', 'wp-element', 'wp-is-shallow-equal', + 'wp-priority-queue', 'wp-redux-routine', ), 'wp-date' => array( @@ -210,6 +211,7 @@ 'wp-element', 'wp-hooks', ), + 'wp-priority-queue' => array(), 'wp-redux-routine' => array(), 'wp-rich-text' => array( 'lodash', diff --git a/package-lock.json b/package-lock.json index 07c5034f78385..1566caa308054 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2469,6 +2469,7 @@ "@wordpress/compose": "file:packages/compose", "@wordpress/element": "file:packages/element", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", + "@wordpress/priority-queue": "file:packages/priority-queue", "@wordpress/redux-routine": "file:packages/redux-routine", "equivalent-key-map": "^0.2.2", "is-promise": "^2.1.0", @@ -2766,6 +2767,12 @@ "postcss-color-function": "^4.0.1" } }, + "@wordpress/priority-queue": { + "version": "file:packages/priority-queue", + "requires": { + "@babel/runtime": "^7.0.0" + } + }, "@wordpress/redux-routine": { "version": "file:packages/redux-routine", "requires": { diff --git a/package.json b/package.json index 57e7805e49726..05f1da17f29ba 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@wordpress/notices": "file:packages/notices", "@wordpress/nux": "file:packages/nux", "@wordpress/plugins": "file:packages/plugins", + "@wordpress/priority-queue": "file:packages/priority-queue", "@wordpress/redux-routine": "file:packages/redux-routine", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/shortcode": "file:packages/shortcode", diff --git a/packages/data/package.json b/packages/data/package.json index 7186a0c69fba5..399cc933e689a 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -25,6 +25,7 @@ "@wordpress/compose": "file:../compose", "@wordpress/element": "file:../element", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", + "@wordpress/priority-queue": "file:../priority-queue", "@wordpress/redux-routine": "file:../redux-routine", "equivalent-key-map": "^0.2.2", "is-promise": "^2.1.0", diff --git a/packages/data/src/components/async-mode-provider/index.js b/packages/data/src/components/async-mode-provider/index.js new file mode 100644 index 0000000000000..30e877d0f1ed3 --- /dev/null +++ b/packages/data/src/components/async-mode-provider/index.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +const { Consumer, Provider } = createContext( false ); + +export const AsyncModeConsumer = Consumer; + +export default Provider; diff --git a/packages/data/src/components/with-select/index.js b/packages/data/src/components/with-select/index.js index 37be4f3d2b2da..8820238e9f390 100644 --- a/packages/data/src/components/with-select/index.js +++ b/packages/data/src/components/with-select/index.js @@ -4,11 +4,15 @@ import { Component } from '@wordpress/element'; import { isShallowEqualObjects } from '@wordpress/is-shallow-equal'; import { createHigherOrderComponent } from '@wordpress/compose'; +import { createQueue } from '@wordpress/priority-queue'; /** * Internal dependencies */ import { RegistryConsumer } from '../registry-provider'; +import { AsyncModeConsumer } from '../async-mode-provider'; + +const renderQueue = createQueue(); /** * Higher-order component used to inject state-derived props using registered @@ -70,16 +74,23 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped componentWillUnmount() { this.canRunSelection = false; this.unsubscribe(); + renderQueue.flush( this ); } shouldComponentUpdate( nextProps, nextState ) { // Cycle subscription if registry changes. const hasRegistryChanged = nextProps.registry !== this.props.registry; + const hasSyncRenderingChanged = nextProps.isAsync !== this.props.isAsync; + if ( hasRegistryChanged ) { this.unsubscribe(); this.subscribe( nextProps.registry ); } + if ( hasSyncRenderingChanged ) { + renderQueue.flush( this ); + } + // Treat a registry change as equivalent to `ownProps`, to reflect // `mergeProps` to rendered component if and only if updated. const hasPropsChanged = ( @@ -89,11 +100,11 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped // Only render if props have changed or merge props have been updated // from the store subscriber. - if ( this.state === nextState && ! hasPropsChanged ) { + if ( this.state === nextState && ! hasPropsChanged && ! hasSyncRenderingChanged ) { return false; } - if ( hasPropsChanged ) { + if ( hasPropsChanged || hasSyncRenderingChanged ) { const nextMergeProps = getNextMergeProps( nextProps ); if ( ! isShallowEqualObjects( this.mergeProps, nextMergeProps ) ) { // If merge props change as a result of the incoming props, @@ -137,7 +148,13 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped } subscribe( registry ) { - this.unsubscribe = registry.subscribe( this.onStoreChange ); + this.unsubscribe = registry.subscribe( () => { + if ( this.props.isAsync ) { + renderQueue.add( this, this.onStoreChange ); + } else { + this.onStoreChange(); + } + } ); } render() { @@ -146,14 +163,19 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped } return ( ownProps ) => ( - - { ( registry ) => ( - + + { ( isAsync ) => ( + + { ( registry ) => ( + + ) } + ) } - + ); }, 'withSelect' ); diff --git a/packages/data/src/index.js b/packages/data/src/index.js index e1b9a5fcb1e55..6574f002d47e4 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -12,6 +12,7 @@ import * as plugins from './plugins'; export { default as withSelect } from './components/with-select'; export { default as withDispatch } from './components/with-dispatch'; export { default as RegistryProvider, RegistryConsumer } from './components/registry-provider'; +export { default as __experimentalAsyncModeProvider } from './components/async-mode-provider'; export { createRegistry } from './registry'; export { plugins }; diff --git a/packages/e2e-tests/specs/plugins/meta-attribute-block.test.js b/packages/e2e-tests/specs/plugins/meta-attribute-block.test.js index 905807501774f..45c8256159979 100644 --- a/packages/e2e-tests/specs/plugins/meta-attribute-block.test.js +++ b/packages/e2e-tests/specs/plugins/meta-attribute-block.test.js @@ -40,9 +40,14 @@ describe( 'Block with a meta attribute', () => { await insertBlock( 'Test Meta Attribute Block' ); await page.keyboard.type( 'Meta Value' ); - const persistedValues = await page.evaluate( () => Array.from( document.querySelectorAll( '.my-meta-input' ) ).map( ( input ) => input.value ) ); - persistedValues.forEach( ( val ) => { - expect( val ).toBe( 'Meta Value' ); + const inputs = await page.$$( '.my-meta-input' ); + await inputs.forEach( async ( input ) => { + // Clicking the input selects the block, + // and selecting the block enables the sync data mode + // as otherwise the asynchronous rerendering of unselected blocks + // may cause the input to have not yet been updated for the other blocks + await input.click(); + expect( await input.getProperty( 'value' ) ).toBe( 'Meta Value' ); } ); } ); } ); diff --git a/packages/editor/src/components/block-list/index.js b/packages/editor/src/components/block-list/index.js index b8e7e2926225f..7e5a1e7eedfb1 100644 --- a/packages/editor/src/components/block-list/index.js +++ b/packages/editor/src/components/block-list/index.js @@ -14,7 +14,11 @@ import { * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { + withSelect, + withDispatch, + __experimentalAsyncModeProvider as AsyncModeProvider, +} from '@wordpress/data'; import { compose } from '@wordpress/compose'; /** @@ -24,6 +28,13 @@ import BlockListBlock from './block'; import BlockListAppender from '../block-list-appender'; import { getBlockDOMNode } from '../../utils/dom'; +const forceSyncUpdates = ( WrappedComponent ) => ( props ) => { + return ( + + + + ); +}; class BlockList extends Component { constructor( props ) { super( props ); @@ -182,23 +193,36 @@ class BlockList extends Component { blockClientIds, rootClientId, isDraggable, + selectedBlockClientId, + multiSelectedBlockClientIds, + hasMultiSelection, } = this.props; return (
- { map( blockClientIds, ( clientId, blockIndex ) => ( - - ) ) } + { map( blockClientIds, ( clientId, blockIndex ) => { + const isBlockInSelection = hasMultiSelection ? + multiSelectedBlockClientIds.includes( clientId ) : + selectedBlockClientId === clientId; + + return ( + + + + ); + } ) }
); @@ -206,6 +230,10 @@ class BlockList extends Component { } export default compose( [ + // This component needs to always be synchronous + // as it's the one changing the async mode + // depending on the block selection. + forceSyncUpdates, withSelect( ( select, ownProps ) => { const { getBlockOrder, @@ -213,6 +241,9 @@ export default compose( [ isMultiSelecting, getMultiSelectedBlocksStartClientId, getMultiSelectedBlocksEndClientId, + getSelectedBlockClientId, + getMultiSelectedBlockClientIds, + hasMultiSelection, } = select( 'core/editor' ); const { rootClientId } = ownProps; @@ -222,6 +253,9 @@ export default compose( [ selectionEnd: getMultiSelectedBlocksEndClientId(), isSelectionEnabled: isSelectionEnabled(), isMultiSelecting: isMultiSelecting(), + selectedBlockClientId: getSelectedBlockClientId(), + multiSelectedBlockClientIds: getMultiSelectedBlockClientIds(), + hasMultiSelection: hasMultiSelection(), }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/priority-queue/.npmrc b/packages/priority-queue/.npmrc new file mode 100644 index 0000000000000..43c97e719a5a8 --- /dev/null +++ b/packages/priority-queue/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/priority-queue/CHANGELOG.md b/packages/priority-queue/CHANGELOG.md new file mode 100644 index 0000000000000..2297c60f404ea --- /dev/null +++ b/packages/priority-queue/CHANGELOG.md @@ -0,0 +1,3 @@ +### 1.0.0 (Unreleased) + +Initial release. diff --git a/packages/priority-queue/README.md b/packages/priority-queue/README.md new file mode 100644 index 0000000000000..2405c6fbd6c99 --- /dev/null +++ b/packages/priority-queue/README.md @@ -0,0 +1,32 @@ +# Priority Queue + +This module allows you to run a queue of callback while on the browser's idle time making sure the higher-priority work is performed first. + +## Installation + +Install the module + +```bash +npm install @wordpress/priority-queue --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats). + +## Usage + +```js +import { createQueue } from '@wordpress/priority-queue'; + +const queue = createQueue(); + +// Context objects. +const ctx1 = {}; +const ctx2 = {}; + +// For a given context in the queue, only the last callback is executed. +queue.add( ctx1, () => console.log( 'This will be printed first' ) ); +queue.add( ctx2, () => console.log( 'This won\'t be printed' ) ); +queue.add( ctx2, () => console.log( 'This will be printed second' ) ); +``` + +

Code is Poetry.

diff --git a/packages/priority-queue/package.json b/packages/priority-queue/package.json new file mode 100644 index 0000000000000..5b0ba8a400fb8 --- /dev/null +++ b/packages/priority-queue/package.json @@ -0,0 +1,29 @@ +{ + "name": "@wordpress/priority-queue", + "version": "1.0.0-alpha.0", + "description": "Generic browser priority queue.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "browser", + "async" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/priority-queue/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "dependencies": { + "@babel/runtime": "^7.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/priority-queue/src/index.js b/packages/priority-queue/src/index.js new file mode 100644 index 0000000000000..5934fe74875ef --- /dev/null +++ b/packages/priority-queue/src/index.js @@ -0,0 +1,50 @@ +const requestIdleCallback = window.requestIdleCallback ? window.requestIdleCallback : window.requestAnimationFrame; + +export const createQueue = () => { + const waitingList = []; + const elementsMap = new WeakMap(); + let isRunning = false; + + const runWaitingList = ( deadline ) => { + do { + if ( waitingList.length === 0 ) { + isRunning = false; + return; + } + + const nextElement = waitingList.shift(); + elementsMap.get( nextElement )(); + elementsMap.delete( nextElement ); + } while ( deadline && deadline.timeRemaining && deadline.timeRemaining() > 0 ); + + requestIdleCallback( runWaitingList ); + }; + + const add = ( element, item ) => { + if ( ! elementsMap.has( element ) ) { + waitingList.push( element ); + } + elementsMap.set( element, item ); + if ( ! isRunning ) { + isRunning = true; + requestIdleCallback( runWaitingList ); + } + }; + + const flush = ( element ) => { + if ( ! elementsMap.has( element ) ) { + return false; + } + + elementsMap.delete( element ); + const index = waitingList.indexOf( element ); + waitingList.splice( index, 1 ); + + return true; + }; + + return { + add, + flush, + }; +};