Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
85be643
Collaboration: Add dedicated database table and storage backend
josephfusco Mar 16, 2026
a4f8b98
Collaboration: Replace sync server with collaboration server
josephfusco Mar 16, 2026
da8317c
Collaboration: Remove legacy post meta storage and post type
josephfusco Mar 16, 2026
b06269e
Collaboration: Wire up bootstrap, feature gate, and cron cleanup
josephfusco Mar 16, 2026
886f0b1
Tests: Add collaboration server tests and remove legacy sync tests
josephfusco Mar 16, 2026
6827989
Collaboration: Use persistent object cache for awareness reads
josephfusco Mar 16, 2026
9bcbfe6
Tests: Fix REST schema and multisite test failures
josephfusco Mar 16, 2026
09d0b86
Tests: Remove erroneous connector fixtures from merge artifact
josephfusco Mar 16, 2026
7455141
Collaboration: Rename update_value column to data
josephfusco Mar 17, 2026
d4e27d4
Collaboration: Add type_client_id index and bump db_version
josephfusco Mar 17, 2026
ef00730
Merge branch 'trunk' into collaboration/single-table
josephfusco Mar 17, 2026
9b45174
Collaboration: Add payload limit constants and request validation
josephfusco Mar 17, 2026
dd319b8
Collaboration: Harden entity permission checks
josephfusco Mar 17, 2026
cd4a69f
Collaboration: Add tests for payload limits and permission hardening
josephfusco Mar 17, 2026
442798f
Collaboration: Apply coding standards and clarifications to table sto…
josephfusco Mar 17, 2026
2e7c177
Collaboration: Clean up stale data and unschedule cron when disabled
josephfusco Mar 17, 2026
24f4fdc
Collaboration: Remove backward-compatible wp-sync/v1 route alias
josephfusco Mar 17, 2026
14ba560
Collaboration: Move implementation details from docblock to code comment
josephfusco Mar 17, 2026
318051f
Collaboration: Remove deprecated wp-sync/v1 route test
josephfusco Mar 17, 2026
543bc6b
Collaboration: Add test for client ID reactivation after awareness ex…
josephfusco Mar 17, 2026
ba4ab78
Revert "Collaboration: Remove deprecated wp-sync/v1 route test"
josephfusco Mar 17, 2026
d833d2f
Revert "Collaboration: Remove backward-compatible wp-sync/v1 route al…
josephfusco Mar 17, 2026
030bbce
Collaboration: Harden storage layer, fix duplicate awareness rows, an…
josephfusco Mar 18, 2026
a5d543d
Collaboration: Add missing maxItems to REST schema fixture
josephfusco Mar 18, 2026
85b7491
Collaboration: Rename remove_updates_up_to_cursor to remove_updates_t…
josephfusco Mar 18, 2026
45c639a
Collaboration: Fix PHPCS alignment warnings in tests
josephfusco Mar 18, 2026
0d3d425
Collaboration: Fix REST schema fixture property order
josephfusco Mar 18, 2026
d812496
Merge branch 'trunk' into collaboration/single-table
josephfusco Mar 18, 2026
20131ec
Merge branch 'trunk' into collaboration/single-table
josephfusco Mar 18, 2026
52fc395
Apply suggestion from @peterwilsoncc
josephfusco Mar 19, 2026
1ef60f8
Update src/wp-includes/collaboration/class-wp-collaboration-table-sto…
josephfusco Mar 19, 2026
a988544
Update src/wp-includes/collaboration.php
josephfusco Mar 19, 2026
1a61f33
Collaboration: Merge upstream trunk and adopt wp_collaboration_enable…
josephfusco Mar 19, 2026
92fdc53
Collaboration: Fix test setup for opt-in default
josephfusco Mar 19, 2026
e7ac605
Collaboration: Add tests for cursor uniqueness, LIKE queries, and typ…
josephfusco Mar 19, 2026
a53685f
Collaboration: Add HTTP method rejection and request validation tests
josephfusco Mar 19, 2026
b55f3ce
Collaboration: Harden tests with exact assertions and split multi-sce…
josephfusco Mar 19, 2026
d4ea091
Collaboration: Fix PHPCS errors in tests
josephfusco Mar 19, 2026
6332302
Collaboration: Merge upstream trunk including post meta race conditio…
josephfusco Mar 19, 2026
cc24661
Merge branch 'trunk' into collaboration/single-table
josephfusco Mar 19, 2026
0be5c73
Merge branch 'trunk' into collaboration/single-table
josephfusco Mar 19, 2026
5f320b6
Merge branch 'trunk' into collaboration/single-table
josephfusco Mar 19, 2026
7bd3079
Collaboration: Add test proving sync writes do not invalidate awarene…
josephfusco Mar 19, 2026
68f2a62
Merge branch 'trunk' into collaboration/single-table
josephfusco Mar 20, 2026
4ccf903
Collaboration: Bucket awareness timestamps to 5-second intervals
josephfusco Mar 20, 2026
78ffe6f
Merge branch 'collaboration/single-table' of github.com:josephfusco/w…
josephfusco Mar 20, 2026
fb48ce3
Merge branch 'trunk' into feature/sync-updates-table
josephfusco Mar 31, 2026
465f1ba
Merge branch 'trunk' into collaboration/single-table
josephfusco Apr 1, 2026
8ae6d5a
Collaboration: Add input validation, compaction test, and remove old …
josephfusco Apr 1, 2026
0298cdd
Tests: Add compaction test for integer client IDs
josephfusco Apr 1, 2026
05e48e9
Collaboration: Fix compactor nomination for integer client IDs
josephfusco Apr 1, 2026
4df841c
Tests: Fix PHPCS coding standards in collaboration tests
josephfusco Apr 1, 2026
e724afb
Tests: Update REST API fixture for client_id schema changes
josephfusco Apr 1, 2026
870e4e0
Tests: Skip collaboration E2E tests when JS runtime is unavailable
josephfusco Apr 1, 2026
3b0be0b
Merge branch 'trunk' into collaboration/single-table
josephfusco Apr 1, 2026
ac5db07
Merge remote-tracking branch 'origin/trunk' into collaboration/single…
josephfusco Apr 2, 2026
f2730e7
Update src/wp-includes/collaboration/class-wp-collaboration-table-sto…
josephfusco Apr 10, 2026
8371c5c
Collaboration: Cap client_id at the storage column width.
josephfusco Apr 10, 2026
6e27b56
Merge branch 'trunk' into collaboration/single-table
josephfusco Apr 10, 2026
8ee5220
Collaboration: Add (room, type, date_gmt) index to the collaboration …
josephfusco Apr 10, 2026
961be20
Merge branch 'collaboration/single-table' of github.com:josephfusco/w…
josephfusco Apr 10, 2026
780f859
Merge branch 'trunk' into collaboration/single-table
josephfusco Apr 10, 2026
d549a14
Merge branch 'trunk' into collaboration/single-table
josephfusco Apr 13, 2026
cec47ab
Merge branch 'trunk' into collaboration/single-table
josephfusco Apr 16, 2026
d95a339
Update tests/qunit/fixtures/wp-api-generated.js.
peterwilsoncc Apr 16, 2026
f71e9a9
Merge branch 'trunk' into collaboration/table-awareness-object-cache
peterwilsoncc Apr 19, 2026
e1e6a88
Do not use table for awareness with persistent object cache.
peterwilsoncc Apr 20, 2026
d6dabe7
Reduce granuality to 10seconds, introduce filter.
peterwilsoncc Apr 20, 2026
facd672
Merge branch 'trunk' into collaboration/table-awareness-object-cache
peterwilsoncc Apr 20, 2026
fc45642
Skip tests that require an object cache not be in use for memcached r…
peterwilsoncc Apr 20, 2026
260773d
Filter options rather than update.
peterwilsoncc Apr 20, 2026
59cfe4e
Reset REST server intance to avoid breaking tests.
peterwilsoncc Apr 20, 2026
13d4f2f
Why did the tests fail to run?
peterwilsoncc Apr 20, 2026
2e35aa2
Use in-memory cache for awarenss when persistent cache isn’t available.
peterwilsoncc Apr 20, 2026
b8da67f
Tests for caching related awareness storage.
peterwilsoncc Apr 20, 2026
ec1fa68
CS: alignment.
peterwilsoncc Apr 20, 2026
2023bfd
Simplify use of in memory cache.
peterwilsoncc Apr 20, 2026
3bbb01f
Let the cache determine how to cache arrays.
peterwilsoncc Apr 20, 2026
cd84de0
Test awareness setter does not destroy.
peterwilsoncc Apr 20, 2026
5b64704
Ensure awareness removes out of date clients from cache.
peterwilsoncc Apr 20, 2026
7bb0404
Collaboration: Replace awareness cache layer with dedicated presence …
josephfusco Apr 20, 2026
8927615
Merge branch 'trunk' into collaboration/presence-api
josephfusco Apr 21, 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 collaboration data cleanup.
if ( wp_is_collaboration_enabled()
&& ! wp_next_scheduled( 'wp_delete_old_collaboration_data' )
&& ! wp_installing()
) {
wp_schedule_event( time(), 'daily', 'wp_delete_old_collaboration_data' );
}

set_screen_options();

$date_format = __( 'F j, Y' );
Expand Down
23 changes: 23 additions & 0 deletions src/wp-admin/includes/schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,29 @@ 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->collaboration (
id bigint(20) unsigned NOT NULL auto_increment,
room varchar($max_index_length) NOT NULL default '',
client_id varchar(32) NOT NULL default '',
user_id bigint(20) unsigned NOT NULL default '0',
data longtext NOT NULL,
date_gmt datetime NOT NULL default '0000-00-00 00:00:00',
PRIMARY KEY (id),
KEY room (room,id),
KEY date_gmt (date_gmt)
) $charset_collate;
CREATE TABLE $wpdb->presence (
id bigint(20) unsigned NOT NULL auto_increment,
room varchar($max_index_length) NOT NULL default '',
client_id varchar($max_index_length) NOT NULL default '',
user_id bigint(20) unsigned NOT NULL default '0',
data text NOT NULL,
date_gmt datetime NOT NULL default '0000-00-00 00:00:00',
PRIMARY KEY (id),
UNIQUE KEY room_client (room,client_id),
KEY date_gmt (date_gmt),
KEY user_id (user_id)
) $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 < 61843 ) {
upgrade_700();
}

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

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

/**
* WordPress Collaboration table.
*
* @since 7.0.0
*
* @var string
*/
public $collaboration;

/**
* WordPress Presence table.
*
* @since 7.0.0
*
* @var string
*/
public $presence;

/**
* WordPress Terms table.
*
Expand Down
40 changes: 37 additions & 3 deletions src/wp-includes/collaboration.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
*
* If the WP_ALLOW_COLLABORATION constant is false,
* collaboration is always disabled regardless of the database option.
* Otherwise, falls back to the 'wp_collaboration_enabled' option.
* Otherwise, the feature requires both the 'wp_collaboration_enabled'
* option and the database schema introduced in db_version 61841.
*
* @since 7.0.0
*
Expand All @@ -20,7 +21,8 @@
function wp_is_collaboration_enabled() {
return (
wp_is_collaboration_allowed() &&
(bool) get_option( 'wp_collaboration_enabled' )
get_option( 'wp_collaboration_enabled' ) &&
get_option( 'db_version' ) >= 61843
);
}

Expand All @@ -34,7 +36,7 @@ function wp_is_collaboration_enabled() {
*
* @since 7.0.0
*
* @return bool Whether real-time collaboration is enabled.
* @return bool Whether real-time collaboration is allowed.
*/
function wp_is_collaboration_allowed() {
if ( ! defined( 'WP_ALLOW_COLLABORATION' ) ) {
Expand Down Expand Up @@ -83,3 +85,35 @@ function wp_collaboration_inject_setting() {
'after'
);
}

/**
* Deletes stale collaboration data from the collaboration table.
*
* Removes update rows older than 7 days. Presence data is cleaned
* separately via wp_delete_expired_presence_data().
*
* @since 7.0.0
*/
function wp_delete_old_collaboration_data() {
global $wpdb;

if ( ! wp_is_collaboration_enabled() ) {
/*
* Collaboration was enabled in the past but has since been disabled.
* Unschedule the cron job prior to clean up so this callback does not
* continue to run.
*/
wp_clear_scheduled_hook( 'wp_delete_old_collaboration_data' );
}

/* Clean up update rows older than 7 days. */
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->collaboration} WHERE date_gmt < %s",
gmdate( 'Y-m-d H:i:s', time() - WEEK_IN_SECONDS )
)
);

/* Clean up expired presence entries. */
wp_delete_expired_presence_data();
}
236 changes: 236 additions & 0 deletions src/wp-includes/collaboration/class-wp-collaboration-table-storage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
<?php
/**
* WP_Collaboration_Table_Storage class
*
* @package WordPress
* @since 7.0.0
*/

/**
* Core class that provides an interface for storing and retrieving
* collaboration updates during a collaborative session.
*
* Update data is stored in the `collaboration` database table as an
* append-only log. Awareness (presence) data is stored separately in
* the `presence` table via the Presence API functions.
*
* This class intentionally fires no actions or filters. Collaboration
* queries run on every poll (0.5–1 s per editor tab), so hook overhead
* would degrade the real-time editing loop for all active sessions.
*
* @since 7.0.0
*
* @access private
*/
class WP_Collaboration_Table_Storage {
/**
* Cache of cursors by room.
*
* @since 7.0.0
* @var array<string, int>
*/
private array $room_cursors = array();

/**
* Cache of update counts by room.
*
* @since 7.0.0
* @var array<string, int>
*/
private array $room_update_counts = array();

/**
* Adds an update to a given room.
*
* @since 7.0.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $room Room identifier.
* @param mixed $update Update data.
* @return bool True on success, false on failure.
*/
public function add_update( string $room, $update ): bool {
global $wpdb;

if ( '' === $room || empty( $update['type'] ) || empty( $update['client_id'] ) ) {
return false;
}

$result = $wpdb->insert(
$wpdb->collaboration,
array(
'room' => $room,
'client_id' => $update['client_id'] ?? '',
'data' => wp_json_encode( $update ),
'date_gmt' => gmdate( 'Y-m-d H:i:s' ),
'user_id' => get_current_user_id(),
),
array( '%s', '%s', '%s', '%s', '%d' )
);

return false !== $result;
}

/**
* Gets awareness state for a given room.
*
* Delegates to the Presence API which uses a dedicated table with
* atomic upserts via UNIQUE KEY (room, client_id).
*
* @since 7.0.0
*
* @param string $room Room identifier.
* @param int $timeout Seconds before an awareness entry is considered expired.
* @return array<int, array> Awareness entries.
*/
public function get_awareness_state( string $room, int $timeout = 30 ): array {
return wp_get_presence( $room, $timeout );
}

/**
* Gets the current cursor for a given room.
*
* The cursor is set during get_updates_after_cursor() and represents the
* maximum row ID at the time updates were retrieved.
*
* @since 7.0.0
*
* @param string $room Room identifier.
* @return int Current cursor for the room.
*/
public function get_cursor( string $room ): int {
return $this->room_cursors[ $room ] ?? 0;
}

/**
* Gets the number of updates stored for a given room.
*
* @since 7.0.0
*
* @param string $room Room identifier.
* @return int Number of updates stored for the room.
*/
public function get_update_count( string $room ): int {
return $this->room_update_counts[ $room ] ?? 0;
}

/**
* Retrieves updates from a room after a given cursor.
*
* @since 7.0.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $room Room identifier.
* @param int $cursor Return updates after this cursor.
* @return array<int, mixed> Updates.
*/
public function get_updates_after_cursor( string $room, int $cursor ): array {
global $wpdb;

/*
* Uses a snapshot approach: captures MAX(id) and COUNT(*) in a single
* query, then fetches rows WHERE id > cursor AND id <= max_id. Updates
* arriving after the snapshot are deferred to the next poll, never lost.
*/

/* Snapshot the current max ID and total row count in a single query. */
$snapshot = $wpdb->get_row(
$wpdb->prepare(
"SELECT COALESCE( MAX( id ), 0 ) AS max_id, COUNT(*) AS total FROM {$wpdb->collaboration} WHERE room = %s",
$room
)
);

if ( ! $snapshot ) {
$this->room_cursors[ $room ] = 0;
$this->room_update_counts[ $room ] = 0;
return array();
}

$max_id = (int) $snapshot->max_id;
$total = (int) $snapshot->total;

$this->room_cursors[ $room ] = $max_id;

if ( 0 === $max_id || $max_id <= $cursor ) {
/*
* Preserve the real row count so the server can still
* trigger compaction when updates have accumulated but
* no new ones arrived since the client's last poll.
*/
$this->room_update_counts[ $room ] = $total;
return array();
}

$this->room_update_counts[ $room ] = $total;

/* Fetch updates after the cursor up to the snapshot boundary. */
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT data FROM {$wpdb->collaboration} WHERE room = %s AND id > %d AND id <= %d ORDER BY id ASC",
$room,
$cursor,
$max_id
)
);

if ( ! is_array( $rows ) ) {
return array();
}

$updates = array();
foreach ( $rows as $row ) {
$decoded = json_decode( $row->data, true );
if ( is_array( $decoded ) ) {
$updates[] = $decoded;
}
}

return $updates;
}

/**
* Removes updates from a room up to and including the given cursor.
*
* @since 7.0.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $room Room identifier.
* @param int $cursor Remove updates up to and including this cursor.
* @return bool True on success, false on failure.
*/
public function remove_updates_through_cursor( string $room, int $cursor ): bool {
global $wpdb;

$result = $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->collaboration} WHERE room = %s AND id <= %d",
$room,
$cursor
)
);

return false !== $result;
}

/**
* Sets awareness state for a given client in a room.
*
* Delegates to the Presence API which uses INSERT ... ON DUPLICATE KEY UPDATE
* for atomic upserts, eliminating race conditions.
*
* @since 7.0.0
*
* @param string $room Room identifier.
* @param string $client_id Client identifier.
* @param array<string, mixed> $state Serializable awareness state for this client.
* @param int $user_id WordPress user ID that owns this client.
* @return bool True on success, false on failure.
*/
public function set_awareness_state( string $room, string $client_id, array $state, int $user_id ): bool {
return wp_set_presence( $room, $client_id, $state, $user_id );
}
}
Loading
Loading