-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Real-time collaboration: Add new REST endpoints, setting, and registered post meta #10894
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
base: trunk
Are you sure you want to change the base?
Real-time collaboration: Add new REST endpoints, setting, and registered post meta #10894
Conversation
|
Hi @chriszarate! 👋 Thank you for your contribution to WordPress! 💖 It looks like this is your first pull request to No one monitors this repository for new pull requests. Pull requests must be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. Pull requests are never merged on GitHub. The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making. More information about how GitHub pull requests can be used to contribute to WordPress can be found in the Core Handbook. Please include automated tests. Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the Automated Testing page in the handbook. If you have not had a chance, please review the Contribute with Code page in the WordPress Core Handbook. The Developer Hub also documents the various coding standards that are followed:
Thank you, |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
|
Is there any tests we can add for the default provider and new functionality? |
|
Can we also provide some documentation on how to override the provider given it's known limitation listed above? |
| /** | ||
| * Injects the real-time collaboration setting into a global variable. | ||
| * | ||
| * @since 6.8.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @since 6.8.0 | |
| * @since 7.0.0 |
TimothyBJacobs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't each room a post?
src/wp-includes/collaboration.php
Outdated
| * | ||
| * @access private | ||
| */ | ||
| function wp_collaboration_register_meta() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have wp_create_initial_post_meta() for this instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in e2da144
| * | ||
| * @since 6.8.0 | ||
| */ | ||
| public function init(): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, create_initial_post_types().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 47fa660
| * @since 7.0.0 | ||
| * @var string | ||
| */ | ||
| const REST_NAMESPACE = 'wp/v2/sync'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const REST_NAMESPACE = 'wp/v2/sync'; | |
| const REST_NAMESPACE = 'wp-sync/v1'; |
We don't have namespaces off of wp/v2. A wp prefixed namespace matches what we do elsewhere in Core for more specialized endpoints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in e6b39fb
| if ( 2 !== count( $type_parts ) ) { | ||
| return new WP_Error( | ||
| 'invalid_room_format', | ||
| 'Invalid room format. Expected: entity_kind/entity_name or entity_kind/entity_name:id', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing translations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 0bb0022
| * @return bool|WP_Error True if user has permission, otherwise WP_Error with details. | ||
| */ | ||
| public function check_permissions( WP_REST_Request $request ) { | ||
| $rooms = $request->get_param( 'rooms' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| $rooms = $request->get_param( 'rooms' ); | |
| $rooms = $request['rooms']; |
Core style is to use the array access notation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in e4666c1
| * @param bool $is_compactor True if this client is nominated to perform compaction. | ||
| * @return array Response data for this room. | ||
| */ | ||
| private function get_updates_after( string $room, int $client_id, int $cursor, bool $is_compactor ): array { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it should be a responsibility of the storage layer. If I want to swap with something that can retrieve results ordered by time, we shouldn't have to load all updates always to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the storage interface and class to take this approach in 9e5cf96. It was a little trickier than I thought, because compaction is an external concern and shouldn't be implemented by storage.
| * @param int $cursor Remove updates with markers < this cursor. | ||
| * @return bool True if this compaction is the latest, false if a newer compaction update exists. | ||
| */ | ||
| private function remove_updates_before_cursor( string $room, int $cursor ): bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly here.
No strong reason. Mostly to avoid creating a large number of posts. Each room corresponds to a synced CRDT document (representing a WordPress entity). Currently supported entity types:
In the future, this could be expanded to include all database entities, including individual taxonomy terms. We might also create rooms to share awareness data at the page or screen level. We intend to enforce limits on this default provider, but probably applied to simultaneous collaborators and not on the overall number of synced entities. Sharing a single post felt prudent but happy to reconsider! I know |
| $object_id = null; | ||
|
|
||
| if ( isset( $object_parts[1] ) ) { | ||
| $object_id = $object_parts[1]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| $object_id = null; | |
| if ( isset( $object_parts[1] ) ) { | |
| $object_id = $object_parts[1]; | |
| } | |
| $object_id = $object_parts[1] ?? null; |
| * @param int $client_id Client identifier. | ||
| * @param int $cursor Return updates after this cursor. | ||
| * @param bool $is_compactor True if this client is nominated to perform compaction. | ||
| * @return array Response data for this room. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @return array Response data for this room. | |
| * @return array<string, mixed> Response data for this room. |
| * @since 6.8.0 | ||
| * @var int|null | ||
| */ | ||
| private static $storage_post_id = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks to PHP 7.4:
| private static $storage_post_id = null; | |
| private static ?int $storage_post_id = null; |
| * | ||
| * Data is stored as post meta on a singleton post of a custom post type. | ||
| * | ||
| * @since 6.8.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The @since versions need to be updated to 7.0.0 in this file.
| * @param string $room Room identifier. | ||
| * @param array $update Sync update. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @param string $room Room identifier. | |
| * @param array $update Sync update. | |
| * @param string $room Room identifier. | |
| * @param array<string, mixed> $update Sync update. |
| * @param string $room Room identifier. | ||
| * @param array $awareness Merged awareness state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @param string $room Room identifier. | |
| * @param array $awareness Merged awareness state. | |
| * @param string $room Room identifier. | |
| * @param array<int, array<string, mixed>> $awareness Merged awareness state. |
| 'posts_per_page' => 1, | ||
| 'post_status' => 'publish', | ||
| 'orderby' => 'ID', | ||
| 'order' => 'ASC', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 'order' => 'ASC', | |
| 'order' => 'ASC', | |
| 'fields' => 'ids', |
| if ( ! empty( $posts ) ) { | ||
| self::$storage_post_id = $posts[0]->ID; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if ( ! empty( $posts ) ) { | |
| self::$storage_post_id = $posts[0]->ID; | |
| $post_id = array_first( $posts ); | |
| if ( is_int( $post_id ) ) { | |
| self::$storage_post_id = $post_id; |
| ) | ||
| ); | ||
|
|
||
| if ( ! is_wp_error( $post_id ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same result, but PHPStan likes this more.
| if ( ! is_wp_error( $post_id ) ) { | |
| if ( is_int( $post_id ) ) { |
| * @return int Post ID. | ||
| */ | ||
| private function get_storage_post_id(): int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be that the wp_insert_post() fails. In that case, null is returned.
| * @return int Post ID. | |
| */ | |
| private function get_storage_post_id(): int { | |
| * @return int|null Post ID. | |
| */ | |
| private function get_storage_post_id(): ?int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Description
Trac ticket: https://core.trac.wordpress.org/ticket/64622
In Gutenberg, we have added support for real-time collaboration using CRDT documents (via the [https://yjs.dev/ Yjs library]). This work has suggested the following additions to WordPress:
A default "sync provider" based on HTTP polling that allows collaborators to share updates with each other. Previously, we relied on WebRTC connections between collaborators for this purpose, but it proved unreliable under many network conditions.
A new registered post meta that allows Gutenberg to persist CRDT documents alongside posts.
A new Writing setting that allows users to opt-in to real-time collaboration.
A behavior change to autosaves is needed. When the the original author is editing a draft post (post_status == 'draft' OR 'auto-draft') and they hold the post lock, the autosave targets the actual post instead of an autosave revision. This puts the post data and the persisted CRDT document out of sync and leads to duplicate updates. When real-time collaboration is enabled, all collaborators must autosave in the same way.
This PR provides a proposed implementation of the changes above. This corresponding Gutenberg PR moves the work from the
experimentaldirectory tolib/compat:WordPress/gutenberg#75366
Cumulative work to add this functionality can be found using this label:
https://github.com/WordPress/gutenberg/issues?q=label%3A%22%5BFeature%5D%20Real-time%20Collaboration%22%20is%3Apr
Testing instructions