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

Simplify meta attribute sources and attempt to prepare a generalization of custom sources #16075

Closed
wants to merge 8 commits into from

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented Jun 10, 2019

This is still WIP (I meant to create a draft PR but clicked the wrong button) :)

@youknowriad youknowriad force-pushed the try/simplify-meta-sources-handling branch from a74a898 to dd14ac6 Compare June 11, 2019 09:37
@@ -12,6 +12,10 @@ import { compose } from '@wordpress/compose';
import withRegistryProvider from './with-registry-provider';

class BlockEditorProvider extends Component {
constructor() {
super( ...arguments );
this.lastPersistedBlocks = [];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not satisfied with this trick (this value being an array) but it's the only way I found right now to fix all the use-cases which are:

  • We need to avoid calling resetBlocks if the new "value" prop received is the same as the one already in the block-editor store (to avoid resetting the "undo" levels). (undo e2e test failing)
  • Sometimes calling some changes like inserting a block with a template (columns block for instance) result in two onChange calls being performed synchronously which will eventually trigger two componentDidUpdate calls but eventually the first componentDidUpdate will call resetBlocks if we only compare against the latest blocks value in the block-editor state causing the editor to reset to the previous blocks value (and not the last one). (columns e2e test failing)

The fix here is a bit fragile though, it basically says: don't call "resetBlocks" unless the new prop received is not one of the last persisted blocks value passed to onChange.

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit concerned about this idea of tracking the "last block update", partly because it adds a fair amount to the implementation to track (and some more cumbersome comparison logic), partly because it locks us into considering updates as only ever applying to one block at a time.

Is this synchronization behavior not something we could be doing as a result of BlockEditorProvider calling its onChange or onInput, assuming that this is a "new" value of blocks, and iterating over each to see if any meta updates have occurred? Is the worry here one of performance in iterating the blocks? Which is granted, but maybe some ways to mitigate like tracking which blocks would have meta attributes in the first place (similar idea as in #12555).

return result;
}, attributes );
}
export function getBlockAttributes( state, clientId ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this refactor makes me quite happy to see 😄 This should serve as a nice optimization.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit disappointed to notice that it doesn't change a lot in terms for performance impact. (The cache bust is the bottleneck right now)

let blocks = parse( content );

// Augment with post attributes
blocks = mapBlocks( blocks, ( block ) => metaSource.synchronize( block, post.meta ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It kinda makes me wonder why we're authoring these sources as if they're some generic interface, when ultimately we call it very explicitly and with a very use-case-specific set of arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, right now it's not the perform generic API, it's a first attempt. I think ideally I'd like something composed of two APIs: one API to the list of changes (edits) to apply to the editable elements (post object and ultimately, template object as well)

const edits = applyBlockAttributesEdits( attributesConfig, block.attributes );
// edits here is somehting like { post: { meta: { key: value } }

the second API is how to update block attributes based on the editable objects (post and tempalte)

const block = fillSourceAttributes( block, post, template )

Still not perfect but you get the idea.

Copy link
Member

@aduth aduth Jun 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thought: What if we treat the source synchronization just as any other store control?

That way, they could yield using existing select controls to retrieve whatever store data they need (like here, the post meta).

for ( let i = 0; i < blocks.length; i++ ) {
	for ( let source of sources ) {
		blocks[ i ] = yield* source.sync( blocks[ i ] );
	}
}

// ...

function* sync( block ) {
	const meta = yield select( 'core/editor', 'getEditedPostAttribute', 'meta' );

	// ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does make sense, we'll be calling the same selector over and over for each block though, so I wonder about performance.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does make sense, we'll be calling the same selector over and over for each block though, so I wonder about performance.

I was thinking about it some more afterward, and at least as far as the looping goes, I was thinking the sources could be defined as named exports from a folder corresponding to the attribute source:

// src/store/sources/index.js
export { meta } from './meta';

// src/store/actions.js
import * as sources from './sources';

for ( let i = 0; i < blocks.length; i++ ) {
	const block = blocks[ i ];
	for ( const schema of getBlockType( block.name ).attributes ) {
		if ( sources[ schema.type ] ) {
			blocks[ i ] = yield* source.sync( blocks[ i ] );
		}
	}
}

To your point, I'd wonder if we could memoize (per sync pass) results from some source "common values" which aren't block specific (like meta).

const sourceValues = {};
for ( let i = 0; i < blocks.length; i++ ) {
	const block = blocks[ i ];
	for ( const schema of getBlockType( block.name ).attributes ) {
		if ( ! sources[ schema.type ] ) {
			continue;
		}

		if ( ! sourceValues.hasOwnProperty( schema.type ) ) {
			sourceValues[ schema.type ] = yield* source.getValues();
		}

		blocks[ i ] = yield* source.sync( blocks[ i ], sourceValues[ schema.type ] );
	}
}

In the above, there's still a potential performance concern from iterating every attribute for every block. Something like #16316 benefits from the fact that this is consolidated into the initial parse phase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, let's try that maybe? this API can remain private for a while.

@@ -727,15 +792,41 @@ export function unlockPostSaving( lockName ) {
*
* @param {Array} blocks Block Array.
* @param {?Object} options Optional options.
*
* @return {Object} Action object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can change this to @yields, which helps preserve the documentation.

@youknowriad
Copy link
Contributor Author

I'm a bit concerned about this idea of tracking the "last block update", partly because it adds a fair amount to the implementation to track (and some more cumbersome comparison logic), partly because it locks us into considering updates as only ever applying to one block at a time.

Conceptually speaking, it's not necessary. I had a concern in terms of performance (having to go throught all the blocks on each update). It could be simplified, we can for instance keep just the "clientIds" of the updated blocks instead of the changes themselves.

At the moment, meta attributes is not the "default" which means even without it, it's probably still performant as we'd ignore the looping entirely but I'm thinking the more we'd go into site building, we'd need more "meta-like" sources and it could become more of an issue.

Is this synchronization behavior not something we could be doing as a result of BlockEditorProvider calling its onChange or onInput, assuming that this is a "new" value of blocks, and iterating over each to see if any meta updates have occurred?

So the performance part is addressed above but the reason it's not in BlockEditorProvider is more "conceptual" in considering the BlockEditorProvider as something that updates blocks and block attributes but doesn't care how these attributes are originally sourced, propagated leaving this responsibility to the editor module instead.

return getFlattenedBlockAttributes( action.blocks );
}

return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to return null here, as it would cause any other action than the ones above to reset state? I guess if the idea is that it's only effective for the single action which updates the block?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I didn't give it too much thoughts but yeah the idea is that we don't care about the value after. I don't think it would harm if we return "state" but I wonder if we'd need to "reset" the value in some actions I might have missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh, this "last changes" thing was consider very experimental. If we follow the approach of this PR, I wouldn't mind if we do a full sync on update. Maybe the performance impact is small? we should measure.

[ action.clientId ]: action.attributes,
};

case 'RESET_BLOCKS':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we need to account for these actions. It seems that interpreting something like a block replacement to trigger meta updates might not always be expected (though conversely, maybe it is).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I don't know. I agree that it's less important in the way we use "RESET_BLOCKS" in Core but there's nothing stopping third-party plugins from using the actions right.

@youknowriad
Copy link
Contributor Author

Closing this for now, as it's now largely covered (and more) in #16402

@youknowriad youknowriad closed this Jul 4, 2019
@youknowriad youknowriad deleted the try/simplify-meta-sources-handling branch July 4, 2019 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Block editor /packages/block-editor [Status] In Progress Tracking issues with work in progress
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants