PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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 0.8.7 All 34 releases
← All changes | includes/presence.php +69 -62 1.0.01.1.10 View file →
@@ -17,11 +17,10 @@
17 17 * - **inactive** — Heartbeat seen within `_offline_after` but no
18 18 * user activity within `_inactive_after`.
19 19 * - **offline** — no Heartbeat in `_offline_after` (default 120s).
20 20 *
21 - * Storage is a single autoload=false option (`_desktop_mode_presence`)
22 - * shaped `array<int user_id, array{ last_seen_ms, last_active_ms }>`.
23 - * Single-row keeps autoload happy and avoids per-user options.
21 + * Storage uses a site-scoped table with one row per user.
22 + * The legacy `_desktop_mode_presence` option is retained for recovery.
24 23 *
25 24 * **Public surface.** PHP helpers:
26 25 *
27 26 * - `openstation_presence_record( $user_id, $active )`
@@ -59,30 +58,18 @@
59 58 * deliberate — it is NOT a half-finished rename.
60 59 */
61 60 const OPENSTATION_PRESENCE_OPTION = '_desktop_mode_presence';
62 61
62 +require_once __DIR__ . '/presence-store.php';
63 +
63 64 /**
64 - * Read the entire presence map. Single autoload=false option.
65 + * Read the current site's presence map.
65 66 *
66 67 * @return array<int,array{last_seen_ms:int,last_active_ms:int}>
67 68 */
68 69 function openstation_presence_get_all() {
69 - $raw = get_option( OPENSTATION_PRESENCE_OPTION, array() );
70 - if ( ! is_array( $raw ) ) {
71 - return array();
72 - }
73 - $out = array();
74 - foreach ( $raw as $uid => $record ) {
75 - $uid = (int) $uid;
76 - if ( $uid <= 0 || ! is_array( $record ) ) {
77 - continue;
78 - }
79 - $out[ $uid ] = array(
80 - 'last_seen_ms' => isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0,
81 - 'last_active_ms' => isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0,
82 - );
83 - }
84 - return $out;
70 + $records = openstation_presence_read_records();
71 + return is_wp_error( $records ) ? array() : $records;
85 72 }
86 73
87 74 /**
88 75 * Record a "user is alive" heartbeat. Bumps `last_seen_ms`. If
@@ -88,19 +75,13 @@
88 75 * Record a "user is alive" heartbeat. Bumps `last_seen_ms`. If
89 76 * `$active` is true, also bumps `last_active_ms` (the user just
90 77 * interacted, not just held a tab open).
91 78 *
92 - * Cheap enough to call every Heartbeat tick: the option write is
93 - * throttled — a bump that neither transitions the computed status
94 - * nor moves a persisted timestamp by at least half the offline
95 - * threshold (capped at 60s) skips the `update_option()` call, so N
96 - * idle users no longer rewrite the shared row every tick. Persisted
97 - * timestamps can therefore lag real activity by up to the throttle
98 - * window — always well inside the offline threshold, so computed
99 - * statuses stay correct. Fires `openstation_presence_recorded` on
100 - * every call (with the fresh, un-throttled record) and
101 - * `openstation_presence_changed` only when the computed status moves
102 - * between `online | inactive | offline`.
79 + * Writes are throttled unless status changes or a persisted timestamp is
80 + * behind by half the offline threshold (capped at 60s). Each write atomically
81 + * merges only this user's timestamps. Fires `openstation_presence_recorded`
82 + * on every accepted bump, including throttled bumps, and
83 + * `openstation_presence_changed` when this call observes a status transition.
103 84 *
104 85 * The `openstation_presence_can_track` filter is the per-user opt-out:
105 86 * a plugin that hides specific accounts (compliance, "set yourself
106 87 * invisible", etc.) returns false to skip the bump entirely.
@@ -107,14 +88,26 @@
107 88 *
108 89 * @param int $user_id User to record.
109 90 * @param bool $active Pass `true` when the heartbeat is paired with
110 91 * explicit user activity (mousedown, keydown).
111 - * @return bool True if recorded; false if vetoed by filter or invalid id.
92 + * @return bool True if accepted; false on invalid id, tracking veto or storage failure.
112 93 */
113 94 function openstation_presence_record( $user_id, $active = true ) {
95 + return true === openstation_presence_record_result( $user_id, $active );
96 +}
97 +
98 +/**
99 + * Record presence while preserving a distinct veto and storage failure result.
100 + *
101 + * @internal
102 + * @param int $user_id User to record.
103 + * @param bool $active Whether this request carries activity.
104 + * @return true|WP_Error
105 + */
106 +function openstation_presence_record_result( $user_id, $active = true ) {
114 107 $user_id = (int) $user_id;
115 108 if ( $user_id <= 0 ) {
116 - return false;
109 + return new WP_Error( 'openstation_presence_invalid_user', __( 'A user id is required.', 'desktop-mode' ) );
117 110 }
118 111
119 112 /**
120 113 * Per-user veto on presence tracking. Return false to skip the
@@ -126,13 +119,16 @@
126 119 * @param int $user_id The user being tracked.
127 120 */
128 121 $can = (bool) apply_filters( 'openstation_presence_can_track', true, $user_id );
129 122 if ( ! $can ) {
130 - return false;
123 + return new WP_Error( 'openstation_presence_tracking_veto' );
131 124 }
132 125
133 - $now_ms = (int) round( microtime( true ) * 1000 );
134 - $all = openstation_presence_get_all();
126 + $now_ms = (int) round( microtime( true ) * 1000 );
127 + $all = openstation_presence_read_records( $user_id );
128 + if ( is_wp_error( $all ) ) {
129 + return $all;
130 + }
135 131 $prev = isset( $all[ $user_id ] ) ? $all[ $user_id ] : array(
136 132 'last_seen_ms' => 0,
137 133 'last_active_ms' => 0,
138 134 );
@@ -145,10 +141,19 @@
145 141
146 142 $next_status = openstation_presence_status_from_record( $next );
147 143
148 144 if ( openstation_presence_should_persist( $all, $user_id, $prev, $prev_status, $next_status, $active, $now_ms ) ) {
149 - $all[ $user_id ] = $next;
150 - update_option( OPENSTATION_PRESENCE_OPTION, $all, false );
145 + $write = $next;
146 + $write['last_active_ms'] = $active ? $now_ms : 0;
147 + if ( ! openstation_presence_write_record( $user_id, $write ) ) {
148 + return new WP_Error( 'openstation_presence_write_failed', __( 'Could not save presence.', 'desktop-mode' ), array( 'status' => 503 ) );
149 + }
150 + $stored = openstation_presence_read_records( $user_id );
151 + if ( is_wp_error( $stored ) ) {
152 + return $stored;
153 + }
154 + $next = $stored[ $user_id ] ?? $prev;
155 + $next_status = openstation_presence_status_from_record( $next );
151 156 }
152 157
153 158 /**
154 159 * Fires on every recorded heartbeat — useful for audit logging
@@ -178,12 +183,9 @@
178 183
179 184 /**
180 185 * Decide whether a presence bump needs to hit the database.
181 186 *
182 - * The presence map is a single shared option row: with N concurrent
183 - * users an unconditional write per Heartbeat tick means N full-row
184 - * rewrites (plus option-cache invalidations) every ~15s, almost all
185 - * of them recording no meaningful change. A bump must persist when:
187 + * A bump must persist when:
186 188 *
187 189 * - the user isn't in the map yet (first sighting),
188 190 * - the computed status transitioned (viewers must see it), or
189 191 * - a persisted timestamp has drifted by at least the throttle
@@ -274,9 +276,10 @@
274 276 * @param int $user_id
275 277 * @return string `online | inactive | offline`
276 278 */
277 279 function openstation_presence_status_for_user( $user_id ) {
278 - $all = openstation_presence_get_all();
280 + $all = openstation_presence_read_records( (int) $user_id );
281 + $all = is_wp_error( $all ) ? array() : $all;
279 282 $record = isset( $all[ (int) $user_id ] ) ? $all[ (int) $user_id ] : array();
280 283 return openstation_presence_status_from_record( (array) $record );
281 284 }
282 285
@@ -357,26 +360,20 @@
357 360 }
358 361
359 362 /**
360 363 * Daily cron: prune presence entries for users idle >14 days.
361 - * Keeps the option compact even on long-running sites.
364 + * Deletes only rows still expired when the statement executes.
362 365 */
363 366 function openstation_presence_cron_prune() {
364 - $all = openstation_presence_get_all();
365 - if ( empty( $all ) ) {
367 + global $wpdb;
368 + // Do not rewrite the shared legacy map if migration is unavailable.
369 + if ( ! openstation_presence_migrate_storage() ) {
366 370 return;
367 371 }
368 - $threshold = (int) round( microtime( true ) * 1000 ) - ( 14 * DAY_IN_SECONDS * 1000 );
369 - $pruned = array();
370 - foreach ( $all as $uid => $record ) {
371 - if ( ( (int) $record['last_seen_ms'] ) < $threshold ) {
372 - continue;
373 - }
374 - $pruned[ (int) $uid ] = $record;
375 - }
376 - if ( count( $pruned ) !== count( $all ) ) {
377 - update_option( OPENSTATION_PRESENCE_OPTION, $pruned, false );
378 - }
372 + $table = openstation_presence_table();
373 + $cutoff = (int) round( microtime( true ) * 1000 ) - 14 * DAY_IN_SECONDS * 1000;
374 + $wpdb->query( $wpdb->prepare( "DELETE FROM $table WHERE last_seen_ms < %d", $cutoff ) );
375 + openstation_presence_invalidate_records();
379 376 }
380 377 add_action( 'desktop_mode_presence_daily_prune', 'openstation_presence_cron_prune' );
381 378
382 379 /**
@@ -422,8 +419,9 @@
422 419 }
423 420 $user_id = (int) get_current_user_id();
424 421 $user_active = ! empty( $data['openstation_user_active'] );
425 422
423 + openstation_presence_migration_tick();
426 424 openstation_presence_record( $user_id, $user_active );
427 425
428 426 // Snapshot the users this viewer is allowed to see — by default
429 427 // all tracked users; plugins can narrow via the
@@ -487,8 +485,9 @@
487 485 * GET /desktop-mode/v1/presence — current snapshot, narrowed by the
488 486 * visibility filter.
489 487 */
490 488 function openstation_presence_rest_get() {
489 + openstation_presence_migration_tick();
491 490 $viewer_id = (int) get_current_user_id();
492 491 $all_ids = array_keys( openstation_presence_get_all() );
493 492 $visible = openstation_presence_visible_users( $all_ids, $viewer_id );
494 493 return rest_ensure_response(
@@ -511,8 +510,9 @@
511 510 * Defaults to `{ active: true }` when neither flag is supplied —
512 511 * the simplest "I'm here" call.
513 512 */
514 513 function openstation_presence_rest_post( WP_REST_Request $request ) {
514 + openstation_presence_migration_tick();
515 515 $user_id = (int) get_current_user_id();
516 516 $active = $request->get_param( 'active' );
517 517 $inactive = (bool) $request->get_param( 'inactive' );
518 518
@@ -519,9 +519,12 @@
519 519 if ( $inactive ) {
520 520 // Set the user immediately to `inactive`: bump last_seen
521 521 // (still alive) but force last_active to zero (no recent
522 522 // interaction).
523 - $all = openstation_presence_get_all();
523 + $all = openstation_presence_read_records( $user_id );
524 + if ( is_wp_error( $all ) ) {
525 + return $all;
526 + }
524 527 $rec = isset( $all[ $user_id ] ) ? $all[ $user_id ] : array(
525 528 'last_seen_ms' => 0,
526 529 'last_active_ms' => 0,
527 530 );
@@ -527,10 +530,11 @@
527 530 );
528 531 $prev_status = openstation_presence_status_from_record( $rec );
529 532 $rec['last_seen_ms'] = (int) round( microtime( true ) * 1000 );
530 533 $rec['last_active_ms'] = 0;
531 - $all[ $user_id ] = $rec;
532 - update_option( OPENSTATION_PRESENCE_OPTION, $all, false );
534 + if ( ! openstation_presence_write_record( $user_id, $rec, true ) ) {
535 + return new WP_Error( 'openstation_presence_write_failed', __( 'Could not save presence.', 'desktop-mode' ), array( 'status' => 503 ) );
536 + }
533 537
534 538 $next_status = openstation_presence_status_from_record( $rec );
535 539 do_action( 'openstation_presence_recorded', $user_id, $rec );
536 540 if ( $next_status !== $prev_status ) {
@@ -536,10 +540,13 @@
536 540 if ( $next_status !== $prev_status ) {
537 541 do_action( 'openstation_presence_changed', $user_id, $next_status, $prev_status );
538 542 }
539 543 } else {
540 - $flag = ( null === $active ) ? true : (bool) $active;
541 - openstation_presence_record( $user_id, $flag );
544 + $flag = ( null === $active ) ? true : (bool) $active;
545 + $result = openstation_presence_record_result( $user_id, $flag );
546 + if ( is_wp_error( $result ) && 'openstation_presence_tracking_veto' !== $result->get_error_code() ) {
547 + return $result;
548 + }
542 549 }
543 550
544 551 return rest_ensure_response( array( 'ok' => true ) );
545 552 }