*/ private array $room_cursors = array(); /** * Cache of update counts by room. * * @since 7.0.0 * @var array */ private array $room_update_counts = array(); /** * Cache of storage post IDs by room hash. * * @since 7.0.0 * @var array */ private static array $storage_post_ids = array(); /** * Adds a sync update to a given room. * * @since 7.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $room Room identifier. * @param mixed $update Sync update. * @return bool True on success, false on failure. */ public function add_update( string $room, $update ): bool { global $wpdb; $post_id = $this->get_storage_post_id( $room ); if ( null === $post_id ) { return false; } // Use direct database operation to avoid cache invalidation performed by // post meta functions (`wp_cache_set_posts_last_changed()` and direct // `wp_cache_delete()` calls). return (bool) $wpdb->insert( $wpdb->postmeta, array( 'post_id' => $post_id, 'meta_key' => self::SYNC_UPDATE_META_KEY, 'meta_value' => wp_json_encode( $update ), ), array( '%d', '%s', '%s' ) ); } /** * Gets awareness state for a given room. * * @since 7.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $room Room identifier. * @return array Awareness state. */ public function get_awareness_state( string $room ): array { global $wpdb; $post_id = $this->get_storage_post_id( $room ); if ( null === $post_id ) { return array(); } // Use direct database operation to avoid updating the post meta cache. // ORDER BY meta_id DESC ensures the latest row wins if duplicates exist // from a past race condition in set_awareness_state(). $meta_value = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = %s ORDER BY meta_id DESC LIMIT 1", $post_id, self::AWARENESS_META_KEY ) ); if ( null === $meta_value ) { return array(); } $awareness = json_decode( $meta_value, true ); if ( ! is_array( $awareness ) ) { return array(); } return array_values( $awareness ); } /** * Sets awareness state for a given room. * * @since 7.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $room Room identifier. * @param array $awareness Serializable awareness state. * @return bool True on success, false on failure. */ public function set_awareness_state( string $room, array $awareness ): bool { global $wpdb; $post_id = $this->get_storage_post_id( $room ); if ( null === $post_id ) { return false; } // Use direct database operation to avoid cache invalidation performed by // post meta functions (`wp_cache_set_posts_last_changed()` and direct // `wp_cache_delete()` calls). // // If two concurrent requests both see no row and both INSERT, the // duplicate is harmless: get_awareness_state() reads the latest row // (ORDER BY meta_id DESC). $meta_id = $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = %s ORDER BY meta_id DESC LIMIT 1", $post_id, self::AWARENESS_META_KEY ) ); if ( $meta_id ) { return (bool) $wpdb->update( $wpdb->postmeta, array( 'meta_value' => wp_json_encode( $awareness ) ), array( 'meta_id' => $meta_id ), array( '%s' ), array( '%d' ) ); } return (bool) $wpdb->insert( $wpdb->postmeta, array( 'post_id' => $post_id, 'meta_key' => self::AWARENESS_META_KEY, 'meta_value' => wp_json_encode( $awareness ), ), array( '%d', '%s', '%s' ) ); } /** * Gets the current cursor for a given room. * * The cursor is set during get_updates_after_cursor() and represents the * highest meta_id seen for the room's sync updates. * * @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 or creates the storage post for a given room. * * 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( 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 for this room. $posts = get_posts( array( 'post_type' => self::POST_TYPE, 'posts_per_page' => 1, 'post_status' => 'publish', 'name' => $room_hash, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', ) ); $post_id = array_first( $posts ); if ( is_int( $post_id ) ) { self::$storage_post_ids[ $room_hash ] = $post_id; return $post_id; } // 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_ids[ $room_hash ] = $post_id; return $post_id; } return null; } /** * 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 sync updates from a room after the 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 (meta_id). * @return array Sync updates. */ public function get_updates_after_cursor( string $room, int $cursor ): array { global $wpdb; $post_id = $this->get_storage_post_id( $room ); if ( null === $post_id ) { $this->room_cursors[ $room ] = 0; $this->room_update_counts[ $room ] = 0; return array(); } // Capture the current room state first so the returned cursor is race-safe. $stats = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS total_updates, COALESCE( MAX(meta_id), 0 ) AS max_meta_id FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s", $post_id, self::SYNC_UPDATE_META_KEY ) ); $total_updates = $stats ? (int) $stats->total_updates : 0; $max_meta_id = $stats ? (int) $stats->max_meta_id : 0; $this->room_update_counts[ $room ] = $total_updates; $this->room_cursors[ $room ] = $max_meta_id; if ( $max_meta_id <= $cursor ) { return array(); } $rows = $wpdb->get_results( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s AND meta_id > %d AND meta_id <= %d ORDER BY meta_id ASC", $post_id, self::SYNC_UPDATE_META_KEY, $cursor, $max_meta_id ) ); if ( ! $rows ) { return array(); } $updates = array(); foreach ( $rows as $row ) { $decoded = json_decode( $row->meta_value, true ); if ( null !== $decoded ) { $updates[] = $decoded; } } return $updates; } /** * Removes updates from a room that are older than the given cursor. * * @since 7.0.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $room Room identifier. * @param int $cursor Remove updates with meta_id < this cursor. * @return bool True on success, false on failure. */ public function remove_updates_before_cursor( string $room, int $cursor ): bool { global $wpdb; $post_id = $this->get_storage_post_id( $room ); if ( null === $post_id ) { return false; } $deleted_rows = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s AND meta_id < %d", $post_id, self::SYNC_UPDATE_META_KEY, $cursor ) ); if ( false === $deleted_rows ) { return false; } return true; } }