Skip to content

RTC Hybrid: Table with awareness in object cache when available.#11599

Open
peterwilsoncc wants to merge 124 commits intoWordPress:trunkfrom
peterwilsoncc:collaboration/table-awareness-object-cache
Open

RTC Hybrid: Table with awareness in object cache when available.#11599
peterwilsoncc wants to merge 124 commits intoWordPress:trunkfrom
peterwilsoncc:collaboration/table-awareness-object-cache

Conversation

@peterwilsoncc
Copy link
Copy Markdown
Contributor

@peterwilsoncc peterwilsoncc commented Apr 20, 2026

This combines to two best performing approaches following feedback from various stakeholders.

  • Single table for collaboration updates
  • Awareness stored in object cache only when a persistent cache is available, no attempt at DB writes is made.
  • Awareness stored in table when a persistent cache is not available, writes to DB.

The awareness approach is taken from that used by transients but uses the new table as the fallback rather than storing the data in the object cache. This is to allow for the table optimized for collaboration to be used.

Forked from #11256
Trac ticket: https://core.trac.wordpress.org/ticket/64696

Props

The following WP.org accounts should be included in the props list in addition to any added by the bot below.

jorbin, czarate, desrosj, zieladam, joefusco, paulkevan, mindctrl, mukesh27, dmonad, westonruter

Use of AI Tools


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

josephfusco and others added 30 commits March 16, 2026 13:02
Introduces the wp_collaboration table for storing real-time editing
data (document states, awareness info, undo history) and the
WP_Collaboration_Table_Storage class that implements all CRUD
operations against it. Bumps the database schema version to 61840.
Replaces WP_HTTP_Polling_Sync_Server with
WP_HTTP_Polling_Collaboration_Server using the wp-collaboration/v1
REST namespace. Switches to string-based client IDs, fixes the
compaction race condition, adds a backward-compatible wp-sync/v1
route alias, and uses UPDATE-then-INSERT for awareness data.
Deletes WP_Sync_Post_Meta_Storage and WP_Sync_Storage interface,
and removes the wp_sync_storage post type registration from post.php.
These are superseded by the dedicated collaboration table.
Adds wp_is_collaboration_enabled() gate, injects the collaboration
setting into the block editor, registers cron event for cleaning up
stale collaboration data, and updates require/include paths for the
new storage and server classes.
Adds 67 PHPUnit tests for WP_HTTP_Polling_Collaboration_Server covering
document sync, awareness, undo/redo, compaction, permissions, cursor
mechanics, race conditions, cron cleanup, and the backward-compatible
wp-sync/v1 route. Adds E2E tests for 3-user presence, sync, and
undo/redo. Removes the old sync server tests. Updates REST schema
setup and fixtures for the new collaboration endpoints.
Adds a cache-first read path to get_awareness_state() following the
transient pattern: check the persistent object cache, fall back to
the database on miss, and prime the cache with the result.

set_awareness_state() updates the cached entries in-place after the
DB write rather than invalidating, so the cache stays warm for the
next reader in the room. This is application-level deduplication:
the shared collaboration table cannot carry a UNIQUE KEY on
(room, client_id) because sync rows need multiple entries per
room+client pair.

Sites without a persistent cache see no behavior change — the
in-memory WP_Object_Cache provides no cross-request benefit but
keeps the code path identical.
Restore the `wp_client_side_media_processing_enabled` filter and the
`finalize` route that were accidentally removed from the REST schema
test. Add the `collaboration` table to the list of tables expected to
be empty after multisite site creation.
The connectors API key entries in wp-api-generated.js were
incorrectly carried over during the trunk merge. Trunk does not
include them in the generated fixtures since the settings are
dynamically registered and not present in the CI test context.
Rename the `update_value` column to `data` in the collaboration table
storage class and tests, and fix array arrow alignment to satisfy PHPCS.

The shorter name is consistent with WordPress meta tables and avoids
confusion with the `update_value()` method in `WP_REST_Meta_Fields`.
Add a composite index on (type, client_id) to the collaboration table
to speed up awareness upserts, which filter on both columns.

Bump $wp_db_version from 61840 to 61841 so existing installations
pick up the schema change via dbDelta on upgrade.
Introduce MAX_BODY_SIZE (16 MB), MAX_ROOMS_PER_REQUEST (50), and
MAX_UPDATE_DATA_SIZE (1 MB) constants to cap request payloads.

Wire a validate_callback on the route to reject oversized request
bodies with a 413, add maxItems to the rooms schema, and replace
the hardcoded maxLength with the new constant.
Reject non-numeric object IDs early in
can_user_collaborate_on_entity_type(). Verify that a post's actual
type matches the room's claimed entity name before granting access.

For taxonomy rooms, confirm the term exists in the specified taxonomy
and simplify the capability check to use assign_term with the
term's object ID.
Cover oversized request body (413), exceeding max rooms (400),
non-numeric object ID, post type mismatch, nonexistent taxonomy
term, and term in the wrong taxonomy.
…rage

Convert consecutive single-line comments to block comment style per
WordPress coding standards, replace forward slashes with colons in
cache keys to avoid ambiguity, hoist `global $wpdb` above the cache
check in `get_awareness_state()`, and clarify the `$cursor` param
docblock in `remove_updates_before_cursor()`.
When collaboration is disabled, run both DELETE queries (sync and
awareness rows) before unscheduling the cron hook so leftover data
is removed. Hoist `global $wpdb` to the top of the function so the
disabled branch can use it. Add a comment noting future persistent
types may also need exclusion from the sync cleanup query.
The wp-sync/v1 namespace was a transitional alias for the Gutenberg
plugin. Remove it so only wp-collaboration/v1 is registered.
The backward-compatible wp-sync/v1 route alias was removed in
24f4fdc, making this test invalid.
The rooms array schema includes a maxItems constraint of 50, but
the committed wp-api-generated.js fixture was missing it, causing
git diff --exit-code to fail on every PHPUnit CI job.
…hrough_cursor

The previous name was ambiguous — it suggested exclusive semantics,
but the query uses inclusive deletion (id <= %d). "through" clearly
communicates the inclusive behavior without needing to read the docblock.
Co-authored-by: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com>
Comment thread src/wp-includes/collaboration/class-wp-collaboration-table-storage.php Outdated
$has_newer_compaction = false;

foreach ( $updates_after_cursor as $existing ) {
if ( self::UPDATE_TYPE_COMPACTION === $existing['type'] ) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Problem: $existing['type'] is not guaranteed to exist because ::get_updates_after_cursor() returns array<int, mixed>. So $existing here is mixed. It should be updated to account for that somehow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

for example:

					if ( is_array( $existing ) && isset( $existing['type'] ) && self::UPDATE_TYPE_COMPACTION === $existing['type'] ) {

peterwilsoncc and others added 2 commits April 29, 2026 09:13
Co-Authored-By: westonruter <westonruter@git.wordpress.org>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Comment thread src/wp-includes/collaboration/class-wp-collaboration-table-storage.php Outdated
Comment thread src/wp-includes/collaboration/class-wp-collaboration-table-storage.php Outdated
Comment thread src/wp-includes/collaboration/class-wp-collaboration-table-storage.php Outdated
Comment thread src/wp-includes/collaboration/class-wp-http-polling-collaboration-server.php Outdated
@alecgeatches
Copy link
Copy Markdown

@peterwilsoncc Howdy! We found a syncing issue fix which has a PR out in WordPress/gutenberg#77980 as well as a backport into the existing sync server in #11716. We can apply the same fix to this PR as well. I had trouble creating a fork based on your fork, so for now here's a diff to show the changes:

For src/wp-includes/collaboration/class-wp-http-polling-collaboration-server.php:

@@ -529,10 +529,13 @@ class WP_HTTP_Polling_Collaboration_Server {
 				}
 
 				/*
-				 * Reaching this point means there's a newer compaction,
-				 * so we can silently ignore this one.
+				 * A newer compaction already advanced the cursor, but we
+				 * can not safely drop an update. The incoming bytes still encode
+				 * operations other clients may not have seen, so store them as a
+				 * regular update. Y.applyUpdateV2 merges state-as-update blobs
+				 * idempotently, so overlap with the existing compaction is safe.
 				 */
-				return true;
+				return $this->add_update( $room, $client_id, self::UPDATE_TYPE_UPDATE, $data );
 
 			case self::UPDATE_TYPE_SYNC_STEP1:
 			case self::UPDATE_TYPE_SYNC_STEP2:

That's the main fix. Additionally, there's a test fix for the compaction change in tests/phpunit/tests/rest-api/rest-collaboration-server.php:

@@ -1094,7 +1094,7 @@ class WP_Test_REST_Collaboration_Server extends WP_Test_REST_Controller_Testcase
 	/**
 	 * @ticket 64696
 	 */
-	public function test_collaboration_stale_compaction_succeeds_when_newer_compaction_exists(): void {
+	public function test_collaboration_stale_compaction_is_stored_as_update_when_newer_compaction_exists(): void {
 		wp_set_current_user( self::$editor_id );
 
 		$room   = $this->get_post_room();
@@ -1124,9 +1124,12 @@ class WP_Test_REST_Collaboration_Server extends WP_Test_REST_Controller_Testcase
 			)
 		);
 
-		// Client 3 sends a stale compaction at cursor 0. The server should find
-		// client 2's compaction in the updates after cursor 0 and silently discard
-		// this one.
+		// Client 3 sends a stale compaction at cursor 0 (mirroring two offline
+		// clients that reconnect from the same baseline cursor). The server
+		// cannot run remove_updates_through_cursor because client 2 has already
+		// advanced the frontier, but the bytes must still be stored as a
+		// regular update so client 3's operations can propagate to other
+		// clients via Yjs state-as-update merging.
 		$stale_compaction = array(
 			'type' => 'compaction',
 			'data' => 'c3RhbGU=',
@@ -1139,16 +1142,29 @@ class WP_Test_REST_Collaboration_Server extends WP_Test_REST_Controller_Testcase
 
 		$this->assertSame( 200, $response->get_status() );
 
-		// Verify the newer compaction is preserved and the stale one was not stored.
-		$response    = $this->dispatch_collaboration(
+		// Verify the newer compaction is preserved AND the stale compaction's
+		// bytes were persisted (now as type=update so subsequent compactions
+		// don't trip the has_newer_compaction check).
+		$response = $this->dispatch_collaboration(
 			array(
 				$this->build_room( $room, '4', 0, array( 'user' => 'c4' ) ),
 			)
 		);
-		$update_data = wp_list_pluck( $response->get_data()['rooms'][0]['updates'], 'data' );
+		$updates  = $response->get_data()['rooms'][0]['updates'];
 
+		$update_data = wp_list_pluck( $updates, 'data' );
 		$this->assertContains( 'Y29tcGFjdGVk', $update_data, 'The newer compaction should be preserved.' );
-		$this->assertNotContains( 'c3RhbGU=', $update_data, 'The stale compaction should not be stored.' );
+		$this->assertContains( 'c3RhbGU=', $update_data, 'The stale compaction bytes should be stored so client 3\'s operations propagate.' );
+
+		$stale_entry = null;
+		foreach ( $updates as $entry ) {
+			if ( 'c3RhbGU=' === $entry['data'] ) {
+				$stale_entry = $entry;
+				break;
+			}
+		}
+		$this->assertNotNull( $stale_entry, 'The stale compaction entry should be present in the room.' );
+		$this->assertSame( 'update', $stale_entry['type'], 'The stale compaction should be stored as type=update, not type=compaction.' );
 	}
 
 	/*

Please add these changes to this PR if you can, and my apologies for not finding a better way to PR them to you.

# Conflicts:
#	src/wp-includes/collaboration/class-wp-http-polling-collaboration-server.php
#	tests/phpunit/tests/rest-api/rest-sync-server.php
@peterwilsoncc
Copy link
Copy Markdown
Contributor Author

@alecgeatches I've committed the changes to trunk and merged them in to this branch using b7bc9f1

Are you able to verify the changes to the two files on the relevant commit?

peterwilsoncc and others added 2 commits May 6, 2026 11:05
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants