PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 All 35 releases
← All changes | includes/desktop-files/heartbeat.php +89 -72 0.9.1 → 1.1.11 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Files Heartbeat sync (PHP).
3 + * OpenStation — Files Heartbeat sync (PHP).
4 4 *
5 5 * Piggybacks on the existing WordPress Heartbeat tick — the same
6 6 * channel `presence.php` uses — so connected clients see folder
7 7 * sharing changes and other users' placement edits inside one
@@ -6,21 +6,22 @@
6 6 * channel `presence.php` uses — so connected clients see folder
7 7 * sharing changes and other users' placement edits inside one
8 8 * cross-feature poll instead of N parallel ones.
9 9 *
10 - * Wire format. Client sends `desktop_mode_files_subscribe` keyed
11 - * to two version markers:
10 + * Wire format. Client sends `openstation_files_subscribe` keyed
11 + * to three version markers:
12 12 *
13 13 * {
14 - * desktop_mode_files_subscribe: {
14 + * openstation_files_subscribe: {
15 15 * folderVersions: { '<folderId>': lastSeenUpdatedAtMs, ... },
16 - * placementsVersion: lastSeenUpdatedAtMs
16 + * placementsVersion: lastSeenUpdatedAtMs,
17 + * sharesVersion: lastSeenInvitedAtMs
17 18 * }
18 19 * }
19 20 *
20 21 * Server responds with deltas + tombstones:
21 22 *
22 - * desktop_mode_files: {
23 + * openstation_files: {
23 24 * placements: [ <RestPlacementShape> ], // upserts
24 25 * folders: [ <RestFolderShape> ], // upserts (incl. share-mode flips)
25 26 * removed: {
26 27 * placements: [ ids ],
@@ -25,42 +26,42 @@
25 26 * removed: {
26 27 * placements: [ ids ],
27 28 * folders: [ ids ]
28 29 * },
30 + * shares: {
31 + * pending: [ <RestShareShape + folderName/ownerId/ownerName/ownerAvatar> ]
32 + * },
29 33 * serverTimeMs: int,
30 34 * truncated: bool
31 35 * }
32 36 *
33 - * Truncation kicks in when more than `desktop_mode_files_heartbeat_max_rows`
37 + * Truncation kicks in when more than `openstation_files_heartbeat_max_rows`
34 38 * (default 200) rows match — clients fall back to a full REST
35 39 * resync. The default cap is per-payload, not per-folder, so a
36 40 * massive shared folder doesn't starve other folders' deltas.
37 41 *
38 - * @package WPDesktopMode
39 - * @since 0.9.0
42 + * @package OpenStation
40 43 */
41 44
42 45 defined( 'ABSPATH' ) || exit;
43 46
44 47 /**
45 - * @since 0.9.0
46 - *
47 48 * @param array $response Pre-filtered response.
48 49 * @param array $data Client-sent payload.
49 50 * @return array
50 51 */
51 -function desktop_mode_files_heartbeat_received( $response, $data ) {
52 +function openstation_files_heartbeat_received( $response, $data ) {
52 53 if ( ! is_array( $response ) ) {
53 54 $response = array();
54 55 }
55 - if ( empty( $data['desktop_mode_files_subscribe'] ) || ! is_array( $data['desktop_mode_files_subscribe'] ) ) {
56 + if ( empty( $data['openstation_files_subscribe'] ) || ! is_array( $data['openstation_files_subscribe'] ) ) {
56 57 return $response;
57 58 }
58 - if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) {
59 + if ( ! function_exists( 'openstation_is_enabled' ) || ! openstation_is_enabled() ) {
59 60 return $response;
60 61 }
61 62
62 - $sub = $data['desktop_mode_files_subscribe'];
63 + $sub = $data['openstation_files_subscribe'];
63 64 $folder_v = isset( $sub['folderVersions'] ) && is_array( $sub['folderVersions'] )
64 65 ? $sub['folderVersions']
65 66 : array();
66 67 $plc_v = isset( $sub['placementsVersion'] ) ? (int) $sub['placementsVersion'] : 0;
@@ -75,15 +76,13 @@
75 76 * Filter the per-payload row cap. Lower this on slow links
76 77 * to force REST fallback sooner; raise it for fast-LAN
77 78 * intranets where a fatter Heartbeat is fine.
78 79 *
79 - * @since 0.9.0
80 - *
81 80 * @param int $cap Default 200.
82 81 */
83 - $cap = max( 1, (int) apply_filters( 'desktop_mode_files_heartbeat_max_rows', 200 ) );
82 + $cap = max( 1, (int) apply_filters( 'openstation_files_heartbeat_max_rows', 200 ) );
84 83
85 - $response['desktop_mode_files'] = desktop_mode_files_compute_heartbeat_delta(
84 + $response['openstation_files'] = openstation_files_compute_heartbeat_delta(
86 85 $user_id,
87 86 $folder_v,
88 87 $plc_v,
89 88 $cap,
@@ -90,15 +89,13 @@
90 89 $shr_v
91 90 );
92 91 return $response;
93 92 }
94 -add_filter( 'heartbeat_received', 'desktop_mode_files_heartbeat_received', 5, 2 );
93 +add_filter( 'heartbeat_received', 'openstation_files_heartbeat_received', 5, 2 );
95 94
96 95 /**
97 96 * Compute the delta payload for a viewer.
98 97 *
99 - * @since 0.9.0
100 - *
101 98 * @param int $user_id Viewer.
102 99 * @param array $folder_versions `{ folderId => lastSeenUpdatedAtMs }`.
103 100 * @param int $placements_version Last-seen `updated_at_ms` for placements.
104 101 * @param int $cap Row cap.
@@ -108,19 +105,19 @@
108 105 * to invites the client hasn't seen
109 106 * yet. Defaults to `0` (deliver all).
110 107 * @return array
111 108 */
112 -function desktop_mode_files_compute_heartbeat_delta( $user_id, $folder_versions, $placements_version, $cap, $shares_version = 0 ) {
109 +function openstation_files_compute_heartbeat_delta( $user_id, $folder_versions, $placements_version, $cap, $shares_version = 0 ) {
113 110 global $wpdb;
114 111
115 - $tables = desktop_mode_files_table_names();
112 + $tables = openstation_files_table_names();
116 113 $truncated = false;
117 114
118 115 // 1) Visible folders the viewer should know about. We send
119 - // the FULL row when its `updated_at_ms` exceeds whatever
120 - // the client last saw (or the client doesn't know about
121 - // it at all).
122 - $visible = desktop_mode_files_get_visible_folders( $user_id );
116 + // the FULL row when its `updated_at_ms` exceeds whatever
117 + // the client last saw (or the client doesn't know about
118 + // it at all).
119 + $visible = openstation_files_get_visible_folders( $user_id );
123 120 $folder_upserts = array();
124 121 foreach ( $visible as $row ) {
125 122 $id = (int) $row['id'];
126 123 $client_ts = isset( $folder_versions[ (string) $id ] )
@@ -126,9 +123,9 @@
126 123 $client_ts = isset( $folder_versions[ (string) $id ] )
127 124 ? (int) $folder_versions[ (string) $id ]
128 125 : 0;
129 126 if ( (int) $row['updated_at_ms'] > $client_ts ) {
130 - $folder_upserts[] = desktop_mode_files_shape_folder( $row );
127 + $folder_upserts[] = openstation_files_shape_folder( $row );
131 128 if ( count( $folder_upserts ) >= $cap ) {
132 129 $truncated = true;
133 130 break;
134 131 }
@@ -135,14 +132,17 @@
135 132 }
136 133 }
137 134
138 135 // 2) Placement upserts the viewer can see. We pull anything
139 - // written since `placements_version` whose owner is the
140 - // viewer (their own desktop) OR which lives in a folder
141 - // the viewer can see (shared content).
142 - $visible_folder_ids = array_map( static function ( $f ) {
143 - return (int) $f['id'];
144 - }, $visible );
136 + // written since `placements_version` whose owner is the
137 + // viewer (their own desktop) OR which lives in a folder
138 + // the viewer can see (shared content).
139 + $visible_folder_ids = array_map(
140 + static function ( $f ) {
141 + return (int) $f['id'];
142 + },
143 + $visible
144 + );
145 145 // Always include the desktop root (parent_id=0) for the viewer.
146 146 $placement_upserts = array();
147 147 if ( ! $truncated ) {
148 148 // Owner-or-visible-folder filter, expressed as a SINGLE
@@ -196,16 +196,16 @@
196 196 ARRAY_A
197 197 );
198 198 }
199 199 foreach ( (array) $rows as $row ) {
200 - $row = desktop_mode_files_normalize_placement_row( $row );
200 + $row = openstation_files_normalize_placement_row( $row );
201 201 // Per-placement read gate: shared folder shouldn't
202 202 // surface a row the viewer's `can_read()` rejects.
203 - $file = desktop_mode_resolve_file( $row['file_type'], $row['file_ref'] );
203 + $file = openstation_resolve_file( $row['file_type'], $row['file_ref'] );
204 204 if ( $file && ! $file->can_read( $user_id ) ) {
205 205 continue;
206 206 }
207 - $placement_upserts[] = desktop_mode_files_shape_placement( $row );
207 + $placement_upserts[] = openstation_files_shape_placement( $row );
208 208 }
209 209 if ( count( $placement_upserts ) >= $cap ) {
210 210 $truncated = true;
211 211 }
@@ -211,9 +211,9 @@
211 211 }
212 212 }
213 213
214 214 // 3) Tombstones since the last placements_version — gives the
215 - // client the "this row is gone" signal.
215 + // client the "this row is gone" signal.
216 216 $tomb_rows = $wpdb->get_results(
217 217 $wpdb->prepare(
218 218 "SELECT kind, ref_id FROM {$tables['tombstones']} WHERE removed_at_ms > %d ORDER BY removed_at_ms ASC LIMIT %d",
219 219 $placements_version,
@@ -220,9 +220,12 @@
220 220 $cap
221 221 ),
222 222 ARRAY_A
223 223 );
224 - $removed = array( 'placements' => array(), 'folders' => array() );
224 + $removed = array(
225 + 'placements' => array(),
226 + 'folders' => array(),
227 + );
225 228 foreach ( (array) $tomb_rows as $row ) {
226 229 if ( 'folder' === $row['kind'] ) {
227 230 $removed['folders'][] = (int) $row['ref_id'];
228 231 } else {
@@ -230,14 +233,14 @@
230 233 }
231 234 }
232 235
233 236 // 4) Soft-trash events. Tombstones only fire on hard delete, so
234 - // a trashed placement / folder would otherwise stay in the
235 - // client store between F5s. Surface every row whose
236 - // `trashed_at_ms` is fresher than the client's high-water
237 - // mark as a `removed.*` entry. Restoring (clearing
238 - // `trashed_at_ms`) bumps `updated_at_ms` and the row will
239 - // flow back through `placements` / `folders` upserts above.
237 + // a trashed placement / folder would otherwise stay in the
238 + // client store between F5s. Surface every row whose
239 + // `trashed_at_ms` is fresher than the client's high-water
240 + // mark as a `removed.*` entry. Restoring (clearing
241 + // `trashed_at_ms`) bumps `updated_at_ms` and the row will
242 + // flow back through `placements` / `folders` upserts above.
240 243 $trashed_placements = $wpdb->get_col(
241 244 $wpdb->prepare(
242 245 "SELECT id FROM {$tables['placements']}
243 246 WHERE trashed_at_ms IS NOT NULL
@@ -266,26 +269,26 @@
266 269 $removed['folders'][] = (int) $id;
267 270 }
268 271
269 272 // 5) Pending share invites for this viewer (across every folder
270 - // they're invited to). Owner-side share-status changes flow
271 - // through the folder upserts above; this channel is for the
272 - // recipient's "you've been invited" placeholder UI.
273 + // they're invited to). Owner-side share-status changes flow
274 + // through the folder upserts above; this channel is for the
275 + // recipient's "you've been invited" placeholder UI.
273 276 $shares = array();
274 - $sharing_enabled = function_exists( 'desktop_mode_files_sharing_enabled_for' )
275 - ? desktop_mode_files_sharing_enabled_for( $user_id )
277 + $sharing_enabled = function_exists( 'openstation_files_sharing_enabled_for' )
278 + ? openstation_files_sharing_enabled_for( $user_id )
276 279 : true;
277 - if ( $sharing_enabled && function_exists( 'desktop_mode_files_get_pending_shares_for_user' ) ) {
278 - $pending = desktop_mode_files_get_pending_shares_for_user( $user_id, $shares_version );
280 + if ( $sharing_enabled && function_exists( 'openstation_files_get_pending_shares_for_user' ) ) {
281 + $pending = openstation_files_get_pending_shares_for_user( $user_id, $shares_version );
279 282 foreach ( $pending as $row ) {
280 - $shape = desktop_mode_files_shape_share( $row );
281 - $folder = desktop_mode_files_get_folder( $row['folder_id'] );
283 + $shape = openstation_files_shape_share( $row );
284 + $folder = openstation_files_get_folder( $row['folder_id'] );
282 285 if ( $folder ) {
283 - $shape['folderName'] = (string) $folder['name'];
284 - $shape['ownerId'] = (int) $folder['owner_id'];
285 - $owner_user = get_userdata( (int) $folder['owner_id'] );
286 - $shape['ownerName'] = $owner_user ? $owner_user->display_name : '';
287 - $shape['ownerAvatar'] = $owner_user ? get_avatar_url( $owner_user->ID, array( 'size' => 48 ) ) : '';
286 + $shape['folderName'] = (string) $folder['name'];
287 + $shape['ownerId'] = (int) $folder['owner_id'];
288 + $owner_user = get_userdata( (int) $folder['owner_id'] );
289 + $shape['ownerName'] = $owner_user ? $owner_user->display_name : '';
290 + $shape['ownerAvatar'] = $owner_user ? get_avatar_url( $owner_user->ID, array( 'size' => 48 ) ) : '';
288 291 }
289 292 $shares[] = $shape;
290 293 if ( count( $shares ) >= $cap ) {
291 294 $truncated = true;
@@ -292,8 +295,22 @@
292 295 break;
293 296 }
294 297 }
295 298 }
299 + // Pending FILE-share invites ride the same channel. Shapes carry
300 + // `targetType: 'file'` + `fileId` / `fileName` so the client
301 + // invite banner can branch (folder shapes have no targetType and
302 + // default to folder handling).
303 + if ( $sharing_enabled && ! $truncated && function_exists( 'openstation_files_get_pending_file_shares_for_user' ) ) {
304 + $pending_files = openstation_files_get_pending_file_shares_for_user( $user_id, $shares_version );
305 + foreach ( $pending_files as $row ) {
306 + $shares[] = openstation_files_shape_file_share( $row );
307 + if ( count( $shares ) >= $cap ) {
308 + $truncated = true;
309 + break;
310 + }
311 + }
312 + }
296 313
297 314 // Safety net: a row that is currently being delivered as an
298 315 // upsert (alive) must NOT also appear in `removed.*`. Otherwise
299 316 // the client applies upserts first, then removals, and the
@@ -305,17 +322,19 @@
305 322 // restored but any tombstones written in error during the trash
306 323 // path stay in the table). Cleaning them up server-side prevents
307 324 // the same client-side glitch on every subsequent tick.
308 325 $upsert_placement_ids = array_map(
309 - static function ( $p ) { return (int) $p['id']; },
326 + static function ( $p ) {
327 + return (int) $p['id']; },
310 328 $placement_upserts
311 329 );
312 - $upsert_folder_ids = array_map(
313 - static function ( $f ) { return (int) $f['id']; },
330 + $upsert_folder_ids = array_map(
331 + static function ( $f ) {
332 + return (int) $f['id']; },
314 333 $folder_upserts
315 334 );
316 335 if ( ! empty( $upsert_placement_ids ) ) {
317 - $alive_placements = array_flip( $upsert_placement_ids );
336 + $alive_placements = array_flip( $upsert_placement_ids );
318 337 $removed['placements'] = array_values(
319 338 array_filter(
320 339 $removed['placements'],
321 340 static function ( $id ) use ( $alive_placements ) {
@@ -325,12 +344,12 @@
325 344 );
326 345 // Cleanup: drop any tombstones referring to placement ids
327 346 // that are demonstrably alive in this tick. Bounded by the
328 347 // upsert set so the work is per-tick, not table-wide.
329 - desktop_mode_files_purge_stale_tombstones( 'placement', $upsert_placement_ids );
348 + openstation_files_purge_stale_tombstones( 'placement', $upsert_placement_ids );
330 349 }
331 350 if ( ! empty( $upsert_folder_ids ) ) {
332 - $alive_folders = array_flip( $upsert_folder_ids );
351 + $alive_folders = array_flip( $upsert_folder_ids );
333 352 $removed['folders'] = array_values(
334 353 array_filter(
335 354 $removed['folders'],
336 355 static function ( $id ) use ( $alive_folders ) {
@@ -337,9 +356,9 @@
337 356 return ! isset( $alive_folders[ (int) $id ] );
338 357 }
339 358 )
340 359 );
341 - desktop_mode_files_purge_stale_tombstones( 'folder', $upsert_folder_ids );
360 + openstation_files_purge_stale_tombstones( 'folder', $upsert_folder_ids );
342 361 }
343 362
344 363 return array(
345 364 'placements' => $placement_upserts,
@@ -347,9 +366,9 @@
347 366 'removed' => $removed,
348 367 'shares' => array(
349 368 'pending' => $shares,
350 369 ),
351 - 'serverTimeMs' => desktop_mode_files_now_ms(),
370 + 'serverTimeMs' => openstation_files_now_ms(),
352 371 'truncated' => $truncated,
353 372 );
354 373 }
355 374
@@ -359,19 +378,17 @@
359 378 * `trashed_at_ms`). One-shot cleanup of stale rows written by
360 379 * earlier buggy code paths — once removed, the heartbeat no longer
361 380 * surfaces them every tick.
362 381 *
363 - * @since 0.18.0
364 - *
365 382 * @param string $kind 'placement' | 'folder'.
366 383 * @param int[] $ids Ids known to be alive in the current tick.
367 384 */
368 -function desktop_mode_files_purge_stale_tombstones( $kind, $ids ) {
385 +function openstation_files_purge_stale_tombstones( $kind, $ids ) {
369 386 if ( empty( $ids ) ) {
370 387 return;
371 388 }
372 389 global $wpdb;
373 - $tables = desktop_mode_files_table_names();
390 + $tables = openstation_files_table_names();
374 391 $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
375 392 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
376 393 $wpdb->query(
377 394 $wpdb->prepare(