Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
317a26e
feat: replace post meta sync storage with dedicated sync_updates table
josephfusco Feb 26, 2026
1bbfc48
Update src/wp-admin/includes/upgrade.php
josephfusco Feb 27, 2026
bd80f59
Guard sync route registration against missing table
josephfusco Feb 27, 2026
5fbf56e
Add WP-Cron cleanup for wp_sync_updates table
josephfusco Feb 27, 2026
a78fcea
Replace room_hash with human-readable room column in sync_updates table
josephfusco Feb 27, 2026
9535e36
Increase sync_updates cron expiration to 7 days and drop filter
josephfusco Feb 27, 2026
547bc5a
Escape room name in sync permission error message
josephfusco Feb 27, 2026
df7b21e
Add tests for cron cleanup, hook registration, and db_version guard
josephfusco Feb 27, 2026
8ed3dd2
Extract awareness transient key into helper with md5 safeguard
josephfusco Feb 27, 2026
42ddcf8
Update tests/phpunit/tests/rest-api/rest-sync-server.php
josephfusco Feb 27, 2026
fea5739
Apply suggestions from code review
josephfusco Feb 27, 2026
f7d5118
Add maxLength validation for sync room name to match DB column size
josephfusco Feb 27, 2026
4ba33c4
Add wp_is_collaboration_enabled() helper
josephfusco Mar 3, 2026
ad6d6ac
Replace inline collaboration checks with wp_is_collaboration_enabled()
josephfusco Mar 3, 2026
c67550b
Add E2E tests for collaborative editing
josephfusco Mar 3, 2026
fc1a56c
Add production-ready docblocks to collaboration files
josephfusco Mar 3, 2026
255cde9
Refactor collaboration E2E tests to use shared utility helpers
josephfusco Mar 3, 2026
51cd57d
Merge branch 'trunk' into feature/sync-updates-table
josephfusco Mar 3, 2026
3296d5d
Apply suggestion from @westonruter
josephfusco Mar 3, 2026
c920b97
Collaboration: Split update_value column into client_id, type, and data.
josephfusco Mar 3, 2026
8d780c2
Merge branch 'feature/sync-updates-table' of https://github.com/josep…
josephfusco Mar 3, 2026
ceccbb4
Tests: Add storage-layer round-trip test for sync_updates column split.
josephfusco Mar 3, 2026
8b71153
Apply suggestion from @westonruter
josephfusco Mar 3, 2026
d1f8026
Collaboration: Restore update_value as opaque blob in sync_updates ta…
josephfusco Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/wp-admin/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@
wp_schedule_event( time(), 'daily', 'delete_expired_transients' );
}

// Schedule sync updates cleanup.
if ( wp_is_collaboration_enabled()
&& ! wp_next_scheduled( 'wp_delete_old_sync_updates' )
&& ! wp_installing()
) {
wp_schedule_event( time(), 'daily', 'wp_delete_old_sync_updates' );
}

set_screen_options();

$date_format = __( 'F j, Y' );
Expand Down
9 changes: 9 additions & 0 deletions src/wp-admin/includes/schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ function wp_get_db_schema( $scope = 'all', $blog_id = null ) {
KEY post_parent (post_parent),
KEY post_author (post_author),
KEY type_status_author (post_type,post_status,post_author)
) $charset_collate;
CREATE TABLE $wpdb->sync_updates (
id bigint(20) unsigned NOT NULL auto_increment,
room varchar(255) NOT NULL,
update_value longtext NOT NULL,
created_at datetime NOT NULL default '0000-00-00 00:00:00',
PRIMARY KEY (id),
KEY room (room,id),
KEY created_at (created_at)
) $charset_collate;\n";

// Single site users table. The multisite flavor of the users table is handled below.
Expand Down
2 changes: 1 addition & 1 deletion src/wp-admin/includes/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ function upgrade_all() {
upgrade_682();
}

if ( $wp_current_db_version < 61644 ) {
if ( $wp_current_db_version < 61698 ) {
upgrade_700();
}

Expand Down
10 changes: 10 additions & 0 deletions src/wp-includes/class-wpdb.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ class wpdb {
'term_relationships',
'termmeta',
'commentmeta',
'sync_updates',
);

/**
Expand Down Expand Up @@ -404,6 +405,15 @@ class wpdb {
*/
public $posts;

/**
* WordPress Sync Updates table.
*
* @since 7.0.0
*
* @var string
*/
public $sync_updates;

/**
* WordPress Terms table.
*
Expand Down
40 changes: 39 additions & 1 deletion src/wp-includes/collaboration.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@
* @since 7.0.0
*/

/**
* Checks whether real-time collaboration is enabled.
*
* The feature requires both the site option and the database schema
* introduced in db_version 61698.
*
* @since 7.0.0
*
* @return bool True if collaboration is enabled, false otherwise.
*/
function wp_is_collaboration_enabled() {
return get_option( 'wp_enable_real_time_collaboration' )
&& get_option( 'db_version' ) >= 61698;
}

/**
* Injects the real-time collaboration setting into a global variable.
*
Expand All @@ -14,11 +29,34 @@
* @access private
*/
function wp_collaboration_inject_setting() {
if ( get_option( 'wp_enable_real_time_collaboration' ) ) {
if ( wp_is_collaboration_enabled() ) {
wp_add_inline_script(
'wp-core-data',
'window._wpCollaborationEnabled = true;',
'after'
);
}
}

/**
* Deletes sync updates older than 7 days from the wp_sync_updates table.
*
* Rows left behind by abandoned collaborative editing sessions are cleaned up
* to prevent unbounded table growth.
*
* @since 7.0.0
*/
function wp_delete_old_sync_updates() {
if ( ! wp_is_collaboration_enabled() ) {
return;
}

global $wpdb;

$wpdb->query(
Copy link

Choose a reason for hiding this comment

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

Given that this cleanup routine is running via cron, I wonder if it should be more resilient to large amounts of data removal - imagine in a scenario of 1,000 updates being removed in one query, we should perhaps batch process this in reasonable amounts or grab the correct data from a select query first.

Copy link
Member

Choose a reason for hiding this comment

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

I think this would definitely be a concern in the case of deleting 1,000 posts or postmeta, given that actions are expected to run for each deletion. But a SQL DELETE like this should be very fast. For prior art, the delete_expired_transients() function does a similar DELETE query and it also lacks any LIMIT or batching. And this is even with option_value not being indexed. To ensure the deletion of wp_sync_updates is as fast or faster, an index could be added to created_at: https://github.com/WordPress/wordpress-develop/pull/11068/changes#r2879822934

Copy link

Choose a reason for hiding this comment

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

Can we assume updates that are a week+ old are safe to delete? I asked Gemini this question and it said:

The wp_delete_old_sync_updates function unconditionally deletes updates older than 7 days.

  • Impact: If a user does not sync for > 7 days, or if the latest "compaction" (snapshot) update is > 7 days old, the server will delete the history they rely on. When the client reconnects, the server will return "no updates" (or a partial list starting after the gap). The client will assume it is up-to-date or apply updates on top of an inconsistent state, leading to document corruption.
  • Context: The server does not signal "History Lost" or force a resync. The removal of history is safer than the old infinite-growth model, but without a protocol to detect "too old" cursors (e.g., checking if requested_cursor < min_table_id), this causes silent data corruption for offline users.

Copy link
Author

@josephfusco josephfusco Mar 3, 2026

Choose a reason for hiding this comment

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

@pkevan @westonruter Agreed - a single DELETE on the indexed created_at column should be fast even at volume, same as delete_expired_transients() handles it today without batching.

@mindctrl The Gemini analysis assumes a client could hold a stale cursor across the 7-day window, but the cursor is in-memory and resets to 0 on every editor load - a returning user always starts fresh.

The behavior here was modeled after wp_delete_auto_drafts(): daily cron, hardcoded 7-day threshold, non-filterable. Auto-drafts are orphaned posts from abandoned editing sessions; sync updates are orphaned rows from abandoned collaboration sessions. Compaction keeps row volume low during active use, so the cleanup is a safety net for abandoned sessions, not a routine data path.

$wpdb->prepare(
"DELETE FROM {$wpdb->sync_updates} WHERE created_at < %s",
gmdate( 'Y-m-d H:i:s', time() - WEEK_IN_SECONDS )
)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* WP_HTTP_Polling_Sync_Server class
*
* @package WordPress
* @since 7.0.0
*/

/**
Expand Down Expand Up @@ -73,6 +74,7 @@ class WP_HTTP_Polling_Sync_Server {
* Storage backend for sync updates.
*
* @since 7.0.0
* @var WP_Sync_Storage
*/
private WP_Sync_Storage $storage;

Expand Down Expand Up @@ -130,9 +132,10 @@ public function register_routes(): void {
'type' => 'integer',
),
'room' => array(
'required' => true,
'type' => 'string',
'pattern' => '^[^/]+/[^/:]+(?::\\S+)?$',
'required' => true,
'type' => 'string',
'pattern' => '^[^/]+/[^/:]+(?::\\S+)?$',
'maxLength' => 255, // The size of the wp_sync_updates.room column.
),
'updates' => array(
'items' => $typed_update_args,
Expand Down Expand Up @@ -198,7 +201,7 @@ public function check_permissions( WP_REST_Request $request ) {
sprintf(
/* translators: %s: The room name encodes the current entity being synced. */
__( 'You do not have permission to sync this entity: %s.' ),
$room
esc_html( $room )
),
array( 'status' => rest_authorization_required_code() )
);
Expand Down
Loading
Loading