RTC Hybrid: Table with awareness in object cache when available.#11599
RTC Hybrid: Table with awareness in object cache when available.#11599peterwilsoncc wants to merge 121 commits intoWordPress:trunkfrom
Conversation
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.
This reverts commit 318051f.
…ias" This reverts commit 24f4fdc.
…d expand test coverage
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>
| // Handle single comment entities with a defined object ID. | ||
| if ( 'root' === $entity_kind && 'comment' === $entity_name && is_numeric( $object_id ) ) { | ||
| return current_user_can( 'edit_comment', (int) $object_id ); | ||
| } |
There was a problem hiding this comment.
There are redundant is_numeric() checks and what's more, it is not checking for zero or negative numbers. This was accounted for previously by wrapping with an is_int( $object_id ) conditional. Why change that?
| $has_newer_compaction = false; | ||
|
|
||
| foreach ( $updates_after_cursor as $existing ) { | ||
| if ( self::UPDATE_TYPE_COMPACTION === $existing['type'] ) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
for example:
if ( is_array( $existing ) && isset( $existing['type'] ) && self::UPDATE_TYPE_COMPACTION === $existing['type'] ) {Co-Authored-By: westonruter <westonruter@git.wordpress.org>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Co-authored-by: Weston Ruter <westonruter@gmail.com>
| * | ||
| * In the event of a race condition, the latest row will be returned as the update target. | ||
| */ | ||
| $exists = $wpdb->get_row( |
There was a problem hiding this comment.
| $exists = $wpdb->get_row( | |
| /** @var object{ collaboration_id: string, date_gmt: string, data: string }|null $exists */ | |
| $exists = $wpdb->get_row( |
(Even though collaboration_id is a bigint(20) in the database, it is a string here.)
| */ | ||
|
|
||
| // Snapshot the current max ID and total row count in a single query. | ||
| /** @var object{ max_id: int, total: int } $snapshot */ |
There was a problem hiding this comment.
I was wrong to suggest these were int:
| /** @var object{ max_id: int, total: int } $snapshot */ | |
| /** @var object{ max_id: string, total: string } $snapshot */ |
| * | ||
| * @access private | ||
| * | ||
| * @phpstan-type AwarenessState array{client_id: string, state: array<string, mixed>, user_id: int, timestamp: int} |
There was a problem hiding this comment.
Very minor, but in \WP_Collaboration_Table_Storage::get_awareness_state() it is supposed to return a list of AwarenessState arrays, but it doesn't validate in any way the shape of state. It just ensures it is an array. Therefore, to not mislead any callers, this is more truthful:
| * @phpstan-type AwarenessState array{client_id: string, state: array<string, mixed>, user_id: int, timestamp: int} | |
| * @phpstan-type AwarenessState array{client_id: string, state: array<mixed, mixed>, user_id: int, timestamp: int} |
I chose array<mixed, mixed> as opposed to just array because the latter is flagged by PHPStan with a strict configuration:
phpstan: Class WP_Collaboration_Table_Storage has type alias AwarenessState with no value type specified in iterable type array.
This is because PHPStan is trying to get the code to be as explicit as possible.
| */ | ||
| private function get_updates( string $room, int $client_id, int $cursor, bool $is_compactor ): array { | ||
| private function get_updates( string $room, string $client_id, int $cursor, bool $is_compactor ): array { | ||
| $updates_after_cursor = $this->storage->get_updates_after_cursor( $room, $cursor ); |
There was a problem hiding this comment.
Since ::get_updates_after_cursor() returns array<int, mixed> we should verify that we only get the updates with the expected types.
| $updates_after_cursor = $this->storage->get_updates_after_cursor( $room, $cursor ); | |
| $updates_after_cursor = array_filter( | |
| $this->storage->get_updates_after_cursor( $room, $cursor ), | |
| static function ( $update ): bool { | |
| return ( | |
| is_array( $update ) | |
| && | |
| isset( $update['client_id'], $update['type'], $update['data'] ) | |
| && | |
| is_string( $update['type'] ) | |
| && | |
| is_string( $update['data'] ) | |
| ); | |
| } | |
| ); |
This combines to two best performing approaches following feedback from various stakeholders.
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.
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.