PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.4
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / modules / Heartbeat / HeartbeatModule.php

HeartbeatModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.4, at includes/modules/Heartbeat/HeartbeatModule.php

226 lines 7.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Heartbeat Control module.
4 *
5 * Controls the WordPress Heartbeat API per context (dashboard / editor /
6 * frontend) and tunes its interval. Disabling heartbeat on the frontend
7 * is one of the cheapest wins for sites that don't need polling there.
8 *
9 * Tier: Free (1:1 with LiteSpeed Cache).
10 * Roadmap: §4.3.
11 *
12 * @package XSpeed
13 */
14
15 declare(strict_types=1);
16
17 namespace XSpeed\Modules\Heartbeat;
18
19 use XSpeed\Module;
20
21 final class HeartbeatModule extends Module {
22
23 public const SLUG = 'heartbeat';
24 public const TIER = self::TIER_FREE;
25 public const VERSION = '1.0.0';
26
27 // Per-context behavior values stored under `behavior_<context>`.
28 public const BEHAVIOR_KEEP = 'keep'; // leave alone
29 public const BEHAVIOR_THROTTLE = 'throttle'; // apply our `frequency`
30 public const BEHAVIOR_DISABLE = 'disable'; // turn off entirely
31
32 private const CONTEXTS = array( 'dashboard', 'editor', 'frontend' );
33
34 public function ui_metadata(): array {
35 return array(
36 'label' => 'Heartbeat',
37 'icon' => 'Activity',
38 'description' => 'Control the WordPress Heartbeat API per context.',
39 );
40 }
41
42 /**
43 * Default profile mirrors the IMPLEMENTATION recommendation:
44 * - frontend: off by default (most polling-heavy, rarely needed)
45 * - editor: throttled to 60s (was 15s)
46 * - dashboard: throttled to 60s (was 60s — left as is)
47 */
48 public function settings_schema(): array {
49 $behavior_options = array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE );
50 $behavior_option_labels = array(
51 self::BEHAVIOR_KEEP => 'Keep',
52 self::BEHAVIOR_THROTTLE => 'Throttle',
53 self::BEHAVIOR_DISABLE => 'Disable',
54 );
55
56 return array(
57 'behavior_dashboard' => array(
58 'type' => 'enum',
59 'default' => self::BEHAVIOR_THROTTLE,
60 'options' => $behavior_options,
61 'option_labels' => $behavior_option_labels,
62 'label' => 'Dashboard',
63 'description' => 'Heartbeat behavior on /wp-admin/ screens (autosave, notifications).',
64 ),
65 'behavior_editor' => array(
66 'type' => 'enum',
67 'default' => self::BEHAVIOR_THROTTLE,
68 'options' => $behavior_options,
69 'option_labels' => $behavior_option_labels,
70 'label' => 'Editor',
71 'description' => 'Heartbeat in the post / block editor. Disable only if you do not need autosave or co-edit locks.',
72 ),
73 'behavior_frontend' => array(
74 'type' => 'enum',
75 'default' => self::BEHAVIOR_DISABLE,
76 'options' => $behavior_options,
77 'option_labels' => $behavior_option_labels,
78 'label' => 'Frontend',
79 'description' => 'Heartbeat on the public site. Recommended off — most themes never need it and it costs admin-ajax requests per visitor.',
80 ),
81 'frequency' => array(
82 'type' => 'int',
83 'default' => 60,
84 'min' => 15,
85 'max' => 300,
86 'label' => 'Throttle Frequency',
87 'description' => 'Interval in seconds for contexts set to Throttle. 60 is a sane default; lower = faster sync but more requests.',
88 ),
89 );
90 }
91
92 // rest_routes — using the Module base-class default (GET + POST under
93 // /xspeed/v1/heartbeat/ wired to rest_get_settings + rest_update_settings).
94
95 public function cli_commands(): array {
96 return array(
97 array(
98 'name' => 'xspeed heartbeat',
99 'callback' => array( $this, 'cli_handler' ),
100 'shortdesc' => 'Inspect or modify xSpeed heartbeat settings.',
101 'synopsis' => array(
102 array(
103 'type' => 'positional',
104 'name' => 'action',
105 'options' => array( 'show', 'set' ),
106 'optional' => false,
107 ),
108 array(
109 'type' => 'assoc',
110 'name' => 'context',
111 'optional' => true,
112 'options' => self::CONTEXTS,
113 ),
114 array(
115 'type' => 'assoc',
116 'name' => 'behavior',
117 'optional' => true,
118 'options' => array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE ),
119 ),
120 array(
121 'type' => 'assoc',
122 'name' => 'frequency',
123 'optional' => true,
124 ),
125 ),
126 ),
127 );
128 }
129
130 public function boot(): void {
131 // `init` is early enough to register our filters before Heartbeat
132 // itself enqueues. Stay low priority to defer to plugins that ran
133 // at plugins_loaded.
134 add_action( 'init', array( $this, 'apply_settings' ), 5 );
135 }
136
137 /**
138 * Hook handler — translates our settings into Heartbeat behavior. Runs
139 * on every request. The branches below are pure read + dispatch — no
140 * I/O so the cost is negligible even on cached requests.
141 */
142 public function apply_settings(): void {
143 $settings = $this->get_settings();
144 $context = $this->detect_context();
145 $behavior = $settings[ 'behavior_' . $context ] ?? self::BEHAVIOR_KEEP;
146
147 if ( self::BEHAVIOR_DISABLE === $behavior ) {
148 // Dequeue the heartbeat script entirely. wp_deregister_script
149 // runs on `init` priority 5 → before wp_default_scripts
150 // (priority 10) re-registers, so we hook the actual enqueue
151 // stage instead.
152 add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_heartbeat' ), 1 );
153 add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_heartbeat' ), 1 );
154 } elseif ( self::BEHAVIOR_THROTTLE === $behavior ) {
155 $interval = max( 15, min( 300, (int) ( $settings['frequency'] ?? 60 ) ) );
156 add_filter(
157 'heartbeat_settings',
158 static function ( $hb_settings ) use ( $interval ) {
159 $hb_settings['interval'] = $interval;
160 return $hb_settings;
161 }
162 );
163 }
164 // BEHAVIOR_KEEP → no filter, WP defaults apply.
165 }
166
167 /**
168 * Dequeue + deregister the heartbeat script. Safe to call multiple
169 * times — both wp functions are idempotent.
170 */
171 public function dequeue_heartbeat(): void {
172 wp_dequeue_script( 'heartbeat' );
173 wp_deregister_script( 'heartbeat' );
174 }
175
176 /**
177 * Classify the current request into dashboard / editor / frontend.
178 * Editor here means the block / classic post editor screens — they're
179 * the heaviest heartbeat consumer and worth their own bucket.
180 */
181 private function detect_context(): string {
182 if ( ! is_admin() ) {
183 return 'frontend';
184 }
185 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
186 if ( $screen && in_array( $screen->base, array( 'post', 'post-new' ), true ) ) {
187 return 'editor';
188 }
189 return 'dashboard';
190 }
191
192 // REST handlers — provided by Module base class
193 // (rest_get_settings / rest_update_settings).
194
195 public function cli_handler( array $args, array $assoc ): void {
196 $action = $args[0] ?? 'show';
197
198 if ( 'show' === $action ) {
199 $opts = $this->get_settings();
200 foreach ( $opts as $key => $value ) {
201 \WP_CLI::log( sprintf( '%-22s %s', $key, is_scalar( $value ) ? (string) $value : wp_json_encode( $value ) ) );
202 }
203 return;
204 }
205
206 if ( 'set' === $action ) {
207 $patch = array();
208 if ( isset( $assoc['context'], $assoc['behavior'] ) ) {
209 $patch[ 'behavior_' . $assoc['context'] ] = $assoc['behavior'];
210 }
211 if ( isset( $assoc['frequency'] ) ) {
212 $patch['frequency'] = (int) $assoc['frequency'];
213 }
214 if ( empty( $patch ) ) {
215 \WP_CLI::error( 'Nothing to set. Provide --context= --behavior= and/or --frequency=' );
216 return;
217 }
218 $this->update_settings( $patch );
219 \WP_CLI::success( 'Updated heartbeat settings.' );
220 return;
221 }
222
223 \WP_CLI::error( "Unknown action: $action" );
224 }
225 }
226