RTC: Add separate doc persistence endpoint#78891
Conversation
…the same entity, tests
…for non-supported entities
|
Size Change: +171 B (0%) Total Size: 8.44 MB 📦 View Changed
|
…groundCRDTSnapshot' which determine whether other clients show a "X user saved" and re-fetch entities
…meta without notifying other users like a user-facing save
…ver round-trip each time the server-side doc is updated as it isn't necessary other than test assertions
…y saved in auto-draft promotion
|
|
||
| if ( get_post_meta( $post_id, self::CRDT_DOC_META_KEY, true ) !== $doc ) { | ||
| $updated = update_post_meta( $post_id, self::CRDT_DOC_META_KEY, $doc ); | ||
| if ( false === $updated ) { |
There was a problem hiding this comment.
update_post_meta internally does the check whether old value is equal to the new value, we don't need to do it separately.
The return value is false in case of equality. That also means that the false return value can't be treated as a strict error. An is_wp_error check on the return value is probably better.
There was a problem hiding this comment.
I didn't know about that behavior without the $prev_value flag! However, it looks like update_post_meta() and the internal update_metadata() function both return int|bool and not a WP_Error. Overall this makes it a bit confusing, as $updated will be false if the document fails to save, but also if the document is the same as already saved. A WP_Error would be much better.
I pushed a change in 3aa0658 that avoids the get_post_meta() call unless we receive false first and then need to differentiate whether the meta change represents an error or a same-value change.
| __( 'You do not have permission to persist this document.', 'gutenberg' ), | ||
| array( 'status' => rest_authorization_required_code() ) | ||
| ); | ||
| } |
There was a problem hiding this comment.
This check is already done in permission_callback, handle_request should only do the business logic.
| } ); | ||
|
|
||
| return { promise, resolve, reject }; | ||
| } |
There was a problem hiding this comment.
There is a native Promise.withResolvers helper for this.
There was a problem hiding this comment.
I fixed this in b2095bf, but ended up reverting it back in cb9c492. I was seeing test failures related to withResolvers():
https://github.com/WordPress/gutenberg/actions/runs/26975840218/job/79602551098?pr=78891
Error: TypeError: Promise.withResolvers is not a function
This passes locally, but it appears we only run CI JS unit tests on node 20, which doesn't have withResolvers().
There was a problem hiding this comment.
Oh, there is Node.js 20 🙁 Let's do it some other day then, when we upgrade Node.
| async function flushPromises() { | ||
| for ( let index = 0; index < 5; index++ ) { | ||
| await Promise.resolve(); | ||
| } |
There was a problem hiding this comment.
await new Promise( r => setTimeout( 0, r ) ) is a more reliable way to do this flush. It will resolve only after all microtasks have reliably run, no matter what is the count.
There was a problem hiding this comment.
Fixed this and the above withResolvers note in b2095bf, thanks!
| */ | ||
| public function validate_request( WP_REST_Request $request ) { | ||
| $body = $request->get_body(); | ||
| if ( is_string( $body ) && strlen( $body ) > self::MAX_BODY_SIZE ) { |
There was a problem hiding this comment.
The MAX_BODY_SIZE is already declared in the args option of register_rest_route. The default validate_callback implementation should therefore do the check, it doesn't need to be implemented separately.
…licitly about testing non-RTC behavior
|
Flaky tests detected in e481c7e. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27150061323
|
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
@danluu Just wanted to note a test change made in this PR from the fuzzer-added tests in #77666, see "Change to waitForConvergence() in E2E test" in the PR description. In short, in addition to checking the live blocks for convergence, we were checking a local |
|
@jsnajdr Thank you for the review notes above! I've addressed them and finalized the PR and description for review. I'll also want to get a couple of other RTC folks in here to do general testing as well. |
jsnajdr
left a comment
There was a problem hiding this comment.
Let's ship
This will fix many existing issues.
| } ); | ||
|
|
||
| return { promise, resolve, reject }; | ||
| } |
There was a problem hiding this comment.
Oh, there is Node.js 20 🙁 Let's do it some other day then, when we upgrade Node.
| return new WP_REST_Response( | ||
| array( | ||
| 'room' => $room, | ||
| ), |
There was a problem hiding this comment.
We don't really use the response, do we? It could be left empty.
Also, wrapping in new WP_REST_Response and 200 status is probably redundant: that's the default behavior. In the default case we only need to return the response array.
| $doc = $request['doc']; | ||
|
|
||
| $updated = update_post_meta( $post_id, self::CRDT_DOC_META_KEY, $doc ); | ||
| if ( false === $updated && get_post_meta( $post_id, self::CRDT_DOC_META_KEY, true ) !== $doc ) { |
There was a problem hiding this comment.
Looking at usages of update_post_meta in the WordPress codebase, the return value is mostly not checked at all 🤷♂️ It's probably quite hard for the update to fail. 🙂
There was a problem hiding this comment.
Okay, good to know. I left in the defensive check for now, and ideally we rarely or never hit that code path anyway.
|
@jsnajdr Addressed the |
What?
Closes #78867.
Add a new
wp-sync/v1/saveendpoint for persisting a CRDT doc outside of a regular post save.Why?
@jsnajdr called out two undesirable behaviors with the current CRDT persistence setup in #78799:
When entities are loaded, we may determine the server-side CRDT document is out of sync with editor content during
getEntityRecord(), and then trigger a subsequentsaveEntityRecord()in order to update the CRDT post meta. Thisgetoperation side effect is unexpected, and has additional side effects both in the editor and on the server:It also changes a post's
modifieddate, which can be incorrect when post content was not saved and only the CRDT meta was updated. We just want to update a persisted CRDT doc, not all of these other actions to occur.We send no-op
{ id }save for non-persisted entities like categories due to a bug:How?
When we're just saving CRDT document snapshots, use the new dedicated
wp-sync/v1/saveREST endpoint and not thesaveEntityRecordsave flow. Directly updating meta avoidswp_update_post(), post-save hooks, editor save UI state, andpost_modifiedchanges for bookkeeping-only CRDT repairs.The sync config has a new
supportsPersistenceflag so we don't need to guess at which entities should receive an update. Unsupported entity rooms such as categories are rejected for CRDT doc persistence and also skipped client side.On the client side,
saveCRDTDoc()serializes the current Yjs document via the sync manager and posts it to/wp-sync/v1/save. Saves are queued per room so a slower, older serialized snapshot cannot complete after a newer one and overwrite it.An important note: User-initiated post saves (e.g. clicking the "Save" button) still include the CRDT doc in the normal post save payload as before. This is explained in the existing
prePersistPostType()call:gutenberg/packages/core-data/src/entities.js
Lines 309 to 313 in cb9c492
Change to
waitForConvergence()in E2E testA small change in behavior worth calling out. Before, on every CRDT save (user-facing or side-effect caused), every user would re-fetch the
_crdt_documentmeta locally. This no longer happens when a side-effect CRDT repair hits/wp-sync/v1/save.The e2e utility function
waitForConvergence()was added as part of fuzzer testing in #77666, and previously had anincludeCrdtDocumentkey that was causing a test failure incollaboration-title-reload.spec.ts. The test failure was specifically due to theincludeCrdtDocument: trueflag only used in this test, and was failing because the page was checking for stalerecord?.meta?._crdt_documentconvergence.This was removed. Previously this worked because a CRDT doc repair generated a "Save" event, which caused all other peers to re-fetch the document from the server. I briefly tried a separate
persistedAtflag to make all other users refetch in the background, but after some investigation it's unnecessary.After the background CRDT repair persists through
/wp-sync/v1/save, the server-side document is correct and users have no need to refetch. The_crdt_documentmeta is otherwise unused in an already active session. The new CRDT doc is still available when a post is loaded from scratch, and explicit post saves still persist the CRDT doc through normal post meta. But once a browser has an active Yjs document, the in-browser source of truth is the Yjs document and editor state, not the local entity record's_crdt_documentfield. So we don't need to force-refresh_crdt_documentas we did before.Testing Instructions
General testing instructions. This will affect a few processes, so some all-around RTC testing would be helpful.
POSTtowp/v2/posts/<post_id>.CRDT repairs
To test a CRDT repair, keep the post open in two browsers and use WPCLI to append some content to the post. Below, replace the ID in
$post_id = <ID>;with the actual post ID:Refresh one of the browsers. After loading, you should see a
wp-sync/v1/saverequest and NOT a request towp/v2/posts/<post_id>. The modified content from DB should be loaded onto the page. The new content should sync between both browsers:repair-sync.mov
Refresh the other browser and ensure that the "Updated content" block is still included in post content.
Automated coverage
Use of AI Tools
AI assistance: Yes
Tool(s): Codex
Used for: The whole process.