Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
105 changes: 49 additions & 56 deletions src/wp-includes/collaboration/class-wp-sync-post-meta-storage.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* Core class that provides an interface for storing and retrieving sync
* updates and awareness data during a collaborative session.
*
* Data is stored as post meta on a singleton post of a custom post type.
* Data is stored as post meta on a dedicated post per room of a custom post type.
*
* @since 7.0.0
*
Expand All @@ -24,6 +24,22 @@ class WP_Sync_Post_Meta_Storage implements WP_Sync_Storage {
*/
const POST_TYPE = 'wp_sync_storage';

/**
* Meta key for awareness state.
*
* @since 7.0.0
* @var string
*/
const AWARENESS_META_KEY = 'wp_sync_awareness';

/**
* Meta key for sync updates.
*
* @since 7.0.0
* @var string
*/
const SYNC_UPDATE_META_KEY = 'wp_sync_update';

/**
* Cache of cursors by room.
*
Expand All @@ -41,12 +57,12 @@ class WP_Sync_Post_Meta_Storage implements WP_Sync_Storage {
private array $room_update_counts = array();

/**
* Singleton post ID for storing sync data.
* Cache of storage post IDs by room hash.
*
* @since 7.0.0
* @var int|null
* @var array<string, int>
*/
private static ?int $storage_post_id = null;
private static array $storage_post_ids = array();

Choose a reason for hiding this comment

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

In practice, will there ever be more than one ID in this array for a given request?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes, because all sync requests for a given client are batched into a single request. If you inspect a request payload, you will see something like:

Screenshot 2026-02-18 at 23 11 37

The post IDs are cached because post meta is accessed multiple times, once for awareness state and at least once for sync updates.


/**
* Adds a sync update to a given room.
Expand All @@ -58,20 +74,18 @@ class WP_Sync_Post_Meta_Storage implements WP_Sync_Storage {
* @return bool True on success, false on failure.
*/
public function add_update( string $room, $update ): bool {
$post_id = $this->get_storage_post_id();
$post_id = $this->get_storage_post_id( $room );
if ( null === $post_id ) {
return false;
}

$meta_key = $this->get_room_meta_key( $room );

// Create an envelope and stamp each update to enable cursor-based filtering.
$envelope = array(
'timestamp' => $this->get_time_marker(),
'value' => $update,
);

return (bool) add_post_meta( $post_id, $meta_key, $envelope, false );
return (bool) add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $envelope, false );
}

/**
Expand All @@ -85,13 +99,12 @@ public function add_update( string $room, $update ): bool {
private function get_all_updates( string $room ): array {
$this->room_cursors[ $room ] = $this->get_time_marker() - 100; // Small buffer to ensure consistency.

$post_id = $this->get_storage_post_id();
$post_id = $this->get_storage_post_id( $room );
if ( null === $post_id ) {
return array();
}

$meta_key = $this->get_room_meta_key( $room );
$updates = get_post_meta( $post_id, $meta_key, false );
$updates = get_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, false );

if ( ! is_array( $updates ) ) {
$updates = array();
Expand Down Expand Up @@ -119,13 +132,12 @@ static function ( $update ): bool {
* @return array<int, mixed> Awareness state.
*/
public function get_awareness_state( string $room ): array {
$post_id = $this->get_storage_post_id();
$post_id = $this->get_storage_post_id( $room );
if ( null === $post_id ) {
return array();
}

$meta_key = $this->get_awareness_meta_key( $room );
$awareness = get_post_meta( $post_id, $meta_key, true );
$awareness = get_post_meta( $post_id, self::AWARENESS_META_KEY, true );

if ( ! is_array( $awareness ) ) {
return array();
Expand All @@ -144,30 +156,16 @@ public function get_awareness_state( string $room ): array {
* @return bool True on success, false on failure.
*/
public function set_awareness_state( string $room, array $awareness ): bool {
$post_id = $this->get_storage_post_id();
$post_id = $this->get_storage_post_id( $room );
if ( null === $post_id ) {
return false;
}

$meta_key = $this->get_awareness_meta_key( $room );

// update_post_meta returns false if the value is the same as the existing value.
update_post_meta( $post_id, $meta_key, $awareness );
update_post_meta( $post_id, self::AWARENESS_META_KEY, $awareness );
return true;
}

/**
* Gets the meta key for a room's awareness state.
*
* @since 7.0.0
*
* @param string $room Room identifier.
* @return string Meta key.
*/
private function get_awareness_meta_key( string $room ): string {
return 'wp_sync_awareness_' . md5( $room );
}

/**
* Gets the current cursor for a given room.
*
Expand All @@ -185,60 +183,56 @@ public function get_cursor( string $room ): int {
}

/**
* Gets the meta key for a room's updates.
*
* @since 7.0.0
* Gets or creates the storage post for a given room.
*
* @param string $room Room identifier.
* @return string Meta key.
*/
private function get_room_meta_key( string $room ): string {
return 'wp_sync_update_' . md5( $room );
}

/**
* Gets or creates the singleton post for storing sync data.
* Each room gets its own dedicated post so that post meta cache
* invalidation is scoped to a single room rather than all of them.
*
* @since 7.0.0
*
* @param string $room Room identifier.
* @return int|null Post ID.
*/
private function get_storage_post_id(): ?int {
if ( is_int( self::$storage_post_id ) ) {
return self::$storage_post_id;
private function get_storage_post_id( string $room ): ?int {
$room_hash = md5( $room );

if ( isset( self::$storage_post_ids[ $room_hash ] ) ) {
return self::$storage_post_ids[ $room_hash ];
}

// Try to find an existing post.
// Try to find an existing post for this room.
$posts = get_posts(
array(
'post_type' => self::POST_TYPE,
'posts_per_page' => 1,
'post_status' => 'publish',
'name' => $room_hash,
'fields' => 'ids',
'order' => 'ASC',
)
);

$post_id = array_first( $posts );
if ( is_int( $post_id ) ) {
self::$storage_post_id = $post_id;
return self::$storage_post_id;
self::$storage_post_ids[ $room_hash ] = $post_id;
return $post_id;
}

// Create new post since none exists.
// Create new post for this room.
$post_id = wp_insert_post(
array(
'post_type' => self::POST_TYPE,
'post_status' => 'publish',
'post_title' => 'Sync Storage',
'post_name' => $room_hash,
)
);

if ( is_int( $post_id ) ) {
self::$storage_post_id = $post_id;
self::$storage_post_ids[ $room_hash ] = $post_id;
return $post_id;
}

return self::$storage_post_id;
return null;
}

/**
Expand Down Expand Up @@ -303,24 +297,23 @@ public function get_updates_after_cursor( string $room, int $cursor ): array {
* @return bool True on success, false on failure.
*/
public function remove_updates_before_cursor( string $room, int $cursor ): bool {
$post_id = $this->get_storage_post_id();
$post_id = $this->get_storage_post_id( $room );
if ( null === $post_id ) {
return false;
}

$all_updates = $this->get_all_updates( $room );
$meta_key = $this->get_room_meta_key( $room );

// Remove all updates for the room and re-store only those that are newer than the cursor.
if ( ! delete_post_meta( $post_id, $meta_key ) ) {
if ( ! delete_post_meta( $post_id, self::SYNC_UPDATE_META_KEY ) ) {
return false;
}

// Re-store envelopes directly to avoid double-wrapping by add_update().
$add_result = true;
foreach ( $all_updates as $envelope ) {
if ( $add_result && $envelope['timestamp'] >= $cursor ) {
$add_result = (bool) add_post_meta( $post_id, $meta_key, $envelope, false );
$add_result = (bool) add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $envelope, false );
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/phpunit/tests/rest-api/rest-sync-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ public function set_up() {
parent::set_up();

// Reset storage post ID cache to ensure clean state after transaction rollback.
$reflection = new ReflectionProperty( 'WP_Sync_Post_Meta_Storage', 'storage_post_id' );
$reflection = new ReflectionProperty( 'WP_Sync_Post_Meta_Storage', 'storage_post_ids' );
if ( PHP_VERSION_ID < 80100 ) {
$reflection->setAccessible( true );
}
$reflection->setValue( null, null );
$reflection->setValue( null, array() );
}

/**
Expand Down