PluginProbe
Gutenberg / 22.7.0
Gutenberg v22.7.0
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / compat / wordpress-7.0 / class-wp-sync-post-meta-storage.php

class-wp-sync-post-meta-storage.php in Gutenberg 22.7.0, at lib/compat/wordpress-7.0/class-wp-sync-post-meta-storage.php

332 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP_Sync_Post_Meta_Storage class
4 *
5 * @package gutenberg
6 */
7
8 if ( ! class_exists( 'WP_Sync_Post_Meta_Storage' ) ) {
9
10 /**
11 * Core class that provides an interface for storing and retrieving sync
12 * updates and awareness data during a collaborative session.
13 *
14 * Data is stored as post meta on a dedicated post per room of a custom post type.
15 *
16 * @since 7.0.0
17 *
18 * @access private
19 */
20 class WP_Sync_Post_Meta_Storage implements WP_Sync_Storage {
21 /**
22 * Post type for sync storage.
23 *
24 * @since 7.0.0
25 * @var string
26 */
27 const POST_TYPE = 'wp_sync_storage';
28
29 /**
30 * Meta key for awareness state.
31 *
32 * @since 7.0.0
33 * @var string
34 */
35 const AWARENESS_META_KEY = 'wp_sync_awareness';
36
37 /**
38 * Meta key for sync updates.
39 *
40 * @since 7.0.0
41 * @var string
42 */
43 const SYNC_UPDATE_META_KEY = 'wp_sync_update';
44
45 /**
46 * Cache of cursors by room.
47 *
48 * @since 7.0.0
49 * @var array<string, int>
50 */
51 private array $room_cursors = array();
52
53 /**
54 * Cache of update counts by room.
55 *
56 * @since 7.0.0
57 * @var array<string, int>
58 */
59 private array $room_update_counts = array();
60
61 /**
62 * Cache of storage post IDs by room hash.
63 *
64 * @since 7.0.0
65 * @var array<string, int>
66 */
67 private static array $storage_post_ids = array();
68
69 /**
70 * Adds a sync update to a given room.
71 *
72 * @since 7.0.0
73 *
74 * @param string $room Room identifier.
75 * @param mixed $update Sync update.
76 * @return bool True on success, false on failure.
77 */
78 public function add_update( string $room, $update ): bool {
79 $post_id = $this->get_storage_post_id( $room );
80 if ( null === $post_id ) {
81 return false;
82 }
83
84 // Create an envelope and stamp each update to enable cursor-based filtering.
85 $envelope = array(
86 'timestamp' => $this->get_time_marker(),
87 'value' => $update,
88 );
89
90 return (bool) add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $envelope, false );
91 }
92
93 /**
94 * Retrieves all sync updates for a given room.
95 *
96 * @since 7.0.0
97 *
98 * @param string $room Room identifier.
99 * @return array<int, array{ timestamp: int, value: mixed }> Sync updates.
100 */
101 private function get_all_updates( string $room ): array {
102 $this->room_cursors[ $room ] = $this->get_time_marker() - 100; // Small buffer to ensure consistency.
103
104 $post_id = $this->get_storage_post_id( $room );
105 if ( null === $post_id ) {
106 return array();
107 }
108
109 $updates = get_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, false );
110
111 if ( ! is_array( $updates ) ) {
112 $updates = array();
113 }
114
115 // Filter out any updates that don't have the expected structure.
116 $updates = array_filter(
117 $updates,
118 static function ( $update ): bool {
119 return is_array( $update ) && isset( $update['timestamp'], $update['value'] ) && is_int( $update['timestamp'] );
120 }
121 );
122
123 $this->room_update_counts[ $room ] = count( $updates );
124
125 return $updates;
126 }
127
128 /**
129 * Gets awareness state for a given room.
130 *
131 * @since 7.0.0
132 *
133 * @param string $room Room identifier.
134 * @return array<int, mixed> Awareness state.
135 */
136 public function get_awareness_state( string $room ): array {
137 $post_id = $this->get_storage_post_id( $room );
138 if ( null === $post_id ) {
139 return array();
140 }
141
142 $awareness = get_post_meta( $post_id, self::AWARENESS_META_KEY, true );
143
144 if ( ! is_array( $awareness ) ) {
145 return array();
146 }
147
148 return array_values( $awareness );
149 }
150
151 /**
152 * Sets awareness state for a given room.
153 *
154 * @since 7.0.0
155 *
156 * @param string $room Room identifier.
157 * @param array<int, mixed> $awareness Serializable awareness state.
158 * @return bool True on success, false on failure.
159 */
160 public function set_awareness_state( string $room, array $awareness ): bool {
161 $post_id = $this->get_storage_post_id( $room );
162 if ( null === $post_id ) {
163 return false;
164 }
165
166 // update_post_meta returns false if the value is the same as the existing value.
167 update_post_meta( $post_id, self::AWARENESS_META_KEY, $awareness );
168 return true;
169 }
170
171 /**
172 * Gets the current cursor for a given room.
173 *
174 * The cursor is set during get_updates_after_cursor() and represents the
175 * point in time just before the updates were retrieved, with a small buffer
176 * to ensure consistency.
177 *
178 * @since 7.0.0
179 *
180 * @param string $room Room identifier.
181 * @return int Current cursor for the room.
182 */
183 public function get_cursor( string $room ): int {
184 return $this->room_cursors[ $room ] ?? 0;
185 }
186
187 /**
188 * Gets or creates the storage post for a given room.
189 *
190 * Each room gets its own dedicated post so that post meta cache
191 * invalidation is scoped to a single room rather than all of them.
192 *
193 * @since 7.0.0
194 *
195 * @param string $room Room identifier.
196 * @return int|null Post ID.
197 */
198 private function get_storage_post_id( string $room ): ?int {
199 $room_hash = md5( $room );
200
201 if ( isset( self::$storage_post_ids[ $room_hash ] ) ) {
202 return self::$storage_post_ids[ $room_hash ];
203 }
204
205 // Try to find an existing post for this room.
206 $posts = get_posts(
207 array(
208 'post_type' => self::POST_TYPE,
209 'posts_per_page' => 1,
210 'post_status' => 'publish',
211 'name' => $room_hash,
212 'fields' => 'ids',
213 )
214 );
215
216 /*
217 * array_first() is a PHP 8.5 function. WordPress added
218 * a polyfill in WP 6.9 (see https://core.trac.wordpress.org/ticket/63853).
219 * Since Gutenberg must support the two most recent WordPress
220 * versions (currently 6.8+), we cannot rely on it here.
221 */
222 $post_id = $posts[0] ?? null;
223 if ( is_int( $post_id ) ) {
224 self::$storage_post_ids[ $room_hash ] = $post_id;
225 return $post_id;
226 }
227
228 // Create new post for this room.
229 $post_id = wp_insert_post(
230 array(
231 'post_type' => self::POST_TYPE,
232 'post_status' => 'publish',
233 'post_title' => 'Sync Storage',
234 'post_name' => $room_hash,
235 )
236 );
237
238 if ( is_int( $post_id ) ) {
239 self::$storage_post_ids[ $room_hash ] = $post_id;
240 return $post_id;
241 }
242
243 return null;
244 }
245
246 /**
247 * Gets the current time in milliseconds as a comparable time marker.
248 *
249 * @since 7.0.0
250 *
251 * @return int Current time in milliseconds.
252 */
253 private function get_time_marker(): int {
254 return (int) floor( microtime( true ) * 1000 );
255 }
256
257 /**
258 * Gets the number of updates stored for a given room.
259 *
260 * @since 7.0.0
261 *
262 * @param string $room Room identifier.
263 * @return int Number of updates stored for the room.
264 */
265 public function get_update_count( string $room ): int {
266 return $this->room_update_counts[ $room ] ?? 0;
267 }
268
269 /**
270 * Retrieves sync updates from a room for a given client and cursor. Updates
271 * from the specified client should be excluded.
272 *
273 * @since 7.0.0
274 *
275 * @param string $room Room identifier.
276 * @param int $cursor Return updates after this cursor.
277 * @return array<int, mixed> Sync updates.
278 */
279 public function get_updates_after_cursor( string $room, int $cursor ): array {
280 $all_updates = $this->get_all_updates( $room );
281 $updates = array();
282
283 foreach ( $all_updates as $update ) {
284 if ( $update['timestamp'] > $cursor ) {
285 $updates[] = $update;
286 }
287 }
288
289 // Sort by timestamp to ensure order.
290 usort(
291 $updates,
292 fn ( $a, $b ) => $a['timestamp'] <=> $b['timestamp']
293 );
294
295 return wp_list_pluck( $updates, 'value' );
296 }
297
298 /**
299 * Removes updates from a room that are older than the given cursor.
300 *
301 * @since 7.0.0
302 *
303 * @param string $room Room identifier.
304 * @param int $cursor Remove updates with markers < this cursor.
305 * @return bool True on success, false on failure.
306 */
307 public function remove_updates_before_cursor( string $room, int $cursor ): bool {
308 $post_id = $this->get_storage_post_id( $room );
309 if ( null === $post_id ) {
310 return false;
311 }
312
313 $all_updates = $this->get_all_updates( $room );
314
315 // Remove all updates for the room and re-store only those that are newer than the cursor.
316 if ( ! delete_post_meta( $post_id, self::SYNC_UPDATE_META_KEY ) ) {
317 return false;
318 }
319
320 // Re-store envelopes directly to avoid double-wrapping by add_update().
321 $add_result = true;
322 foreach ( $all_updates as $envelope ) {
323 if ( $add_result && $envelope['timestamp'] >= $cursor ) {
324 $add_result = (bool) add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $envelope, false );
325 }
326 }
327
328 return $add_result;
329 }
330 }
331 }
332