PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
1.3.7 1.3.6 1.3.5 1.3.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 All 33 releases
xspeed / includes / modules / Heartbeat / HeartbeatModule.php

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

274 lines 9.7 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 defined( 'ABSPATH' ) || exit;
20
21 use XSpeed\Module;
22
23 final class HeartbeatModule extends Module {
24
25 public const SLUG = 'heartbeat';
26 public const TIER = self::TIER_FREE;
27 public const VERSION = '1.0.0';
28
29 // Per-context behavior values stored under `behavior_<context>`.
30 public const BEHAVIOR_KEEP = 'keep'; // leave alone
31 public const BEHAVIOR_THROTTLE = 'throttle'; // apply our `frequency`
32 public const BEHAVIOR_DISABLE = 'disable'; // turn off entirely
33
34 private const CONTEXTS = array( 'dashboard', 'editor', 'frontend' );
35
36 public function ui_metadata(): array {
37 return array(
38 'label' => __( 'Heartbeat', 'xspeed' ),
39 'icon' => 'Activity',
40 'description' => __( 'Controls how often WordPress checks in with your server in the background.', 'xspeed' ),
41 'group' => 'performance',
42 );
43 }
44
45 /**
46 * Default profile mirrors the IMPLEMENTATION recommendation:
47 * - frontend: off by default (most polling-heavy, rarely needed)
48 * - editor: throttled to 60s (was 15s)
49 * - dashboard: throttled to 60s (was 60s — left as is)
50 */
51 public function settings_schema(): array {
52 $behavior_options = array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE );
53 $behavior_option_labels = array(
54 self::BEHAVIOR_KEEP => 'Keep',
55 self::BEHAVIOR_THROTTLE => 'Throttle',
56 self::BEHAVIOR_DISABLE => 'Disable',
57 );
58
59 return array(
60 'behavior_dashboard' => array(
61 'type' => 'enum',
62 'default' => self::BEHAVIOR_THROTTLE,
63 'options' => $behavior_options,
64 'option_labels' => $behavior_option_labels,
65 'label' => __( 'Dashboard', 'xspeed' ),
66 'description' => __( 'Background checks on admin screens, used for notifications.', 'xspeed' ),
67 ),
68 'behavior_editor' => array(
69 'type' => 'enum',
70 'default' => self::BEHAVIOR_THROTTLE,
71 'options' => $behavior_options,
72 'option_labels' => $behavior_option_labels,
73 'label' => __( 'Editor', 'xspeed' ),
74 'description' => __( 'Background checks in the post editor. Disable only if you do not need autosave or the warning when someone else is editing.', 'xspeed' ),
75 ),
76 'behavior_frontend' => array(
77 'type' => 'enum',
78 'default' => self::BEHAVIOR_DISABLE,
79 'options' => $behavior_options,
80 'option_labels' => $behavior_option_labels,
81 'label' => __( 'Frontend', 'xspeed' ),
82 'description' => __( 'Background checks on the public site, which only some plugins add. Disable is recommended, as it cuts extra requests to your server.', 'xspeed' ),
83 ),
84 'frequency' => array(
85 'type' => 'int',
86 'default' => 60,
87 'min' => 15,
88 'max' => 300,
89 'label' => __( 'Throttle interval', 'xspeed' ),
90 'unit' => 'seconds',
91 'description' => __( 'Seconds between checks where you chose Throttle. 60 suits most sites; a lower number means more requests.', 'xspeed' ),
92 'advanced' => true,
93 'dependsOn' => array(
94 'any' => array(
95 array( 'field' => 'behavior_dashboard', 'value' => self::BEHAVIOR_THROTTLE ),
96 array( 'field' => 'behavior_editor', 'value' => self::BEHAVIOR_THROTTLE ),
97 array( 'field' => 'behavior_frontend', 'value' => self::BEHAVIOR_THROTTLE ),
98 ),
99 ),
100 ),
101 );
102 }
103
104 // rest_routes — using the Module base-class default (GET + POST under
105 // /xspeed/v1/heartbeat/ wired to rest_get_settings + rest_update_settings).
106
107 public function cli_commands(): array {
108 return array(
109 array(
110 'name' => 'xspeed heartbeat',
111 'callback' => array( $this, 'cli_handler' ),
112 'shortdesc' => 'Inspect or modify xSpeed heartbeat settings.',
113 'ai_hint' => 'Inspect or change WordPress Heartbeat (admin-ajax polling) frequency. Use for high admin-ajax.php CPU load, or hosting warnings about too many background requests.',
114 'synopsis' => array(
115 array(
116 'type' => 'positional',
117 'name' => 'action',
118 'options' => array( 'show', 'set' ),
119 'optional' => false,
120 ),
121 // Use `--for` (not `--context`): WP-CLI silently swallows a
122 // `--context` assoc arg before it reaches the command, so
123 // `set --context= --behavior=` always lost the context and
124 // errored "Nothing to set". `--for` reads naturally
125 // (set --for=editor --behavior=disable) and is collision-
126 // free. No `options` constraint either — an options list
127 // also dropped args from $assoc; we validate in
128 // cli_handler() instead. (FBS-82153 Bug 2.)
129 array(
130 'type' => 'assoc',
131 'name' => 'for',
132 'optional' => true,
133 ),
134 array(
135 'type' => 'assoc',
136 'name' => 'behavior',
137 'optional' => true,
138 ),
139 array(
140 'type' => 'assoc',
141 'name' => 'frequency',
142 'optional' => true,
143 ),
144 ),
145 ),
146 );
147 }
148
149 public function boot(): void {
150 // Context resolution depends on WHEN we can classify the request:
151 // - Frontend: known at `init` (is_admin() is reliable there), and we
152 // must act before wp_enqueue_scripts (priority 10) registers the
153 // heartbeat script.
154 // - Admin: dashboard-vs-editor needs get_current_screen(), which is
155 // NULL at `init` and only populated from the `current_screen`
156 // action onward. Resolving context at init always returned
157 // "dashboard" on editor screens, so behavior_editor was dead.
158 // (FBS-82153 Bug 1.) Hook admin on `current_screen` instead, which
159 // fires before admin_enqueue_scripts so we can still dequeue.
160 if ( is_admin() ) {
161 add_action( 'current_screen', array( $this, 'apply_settings' ) );
162 } else {
163 add_action( 'init', array( $this, 'apply_settings' ), 5 );
164 }
165 }
166
167 /**
168 * Hook handler — translates our settings into Heartbeat behavior. Runs
169 * on every request. The branches below are pure read + dispatch — no
170 * I/O so the cost is negligible even on cached requests.
171 */
172 public function apply_settings(): void {
173 $settings = $this->get_settings();
174 $context = $this->detect_context();
175 $behavior = $settings[ 'behavior_' . $context ] ?? self::BEHAVIOR_KEEP;
176
177 if ( self::BEHAVIOR_DISABLE === $behavior ) {
178 // Dequeue the heartbeat script entirely. wp_deregister_script
179 // runs on `init` priority 5 → before wp_default_scripts
180 // (priority 10) re-registers, so we hook the actual enqueue
181 // stage instead.
182 add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_heartbeat' ), 1 );
183 add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_heartbeat' ), 1 );
184 } elseif ( self::BEHAVIOR_THROTTLE === $behavior ) {
185 $interval = max( 15, min( 300, (int) ( $settings['frequency'] ?? 60 ) ) );
186 add_filter(
187 'heartbeat_settings',
188 static function ( $hb_settings ) use ( $interval ) {
189 $hb_settings['interval'] = $interval;
190 return $hb_settings;
191 }
192 );
193 }
194 // BEHAVIOR_KEEP → no filter, WP defaults apply.
195 }
196
197 /**
198 * Dequeue + deregister the heartbeat script. Safe to call multiple
199 * times — both wp functions are idempotent.
200 */
201 public function dequeue_heartbeat(): void {
202 wp_dequeue_script( 'heartbeat' );
203 wp_deregister_script( 'heartbeat' );
204 }
205
206 /**
207 * Classify the current request into dashboard / editor / frontend.
208 * Editor here means the block / classic post editor screens — they're
209 * the heaviest heartbeat consumer and worth their own bucket.
210 */
211 private function detect_context(): string {
212 if ( ! is_admin() ) {
213 return 'frontend';
214 }
215 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
216 if ( $screen && in_array( $screen->base, array( 'post', 'post-new' ), true ) ) {
217 return 'editor';
218 }
219 return 'dashboard';
220 }
221
222 // REST handlers — provided by Module base class
223 // (rest_get_settings / rest_update_settings).
224
225 public function cli_handler( array $args, array $assoc ): void {
226 $action = $args[0] ?? 'show';
227
228 if ( 'show' === $action ) {
229 $opts = $this->get_settings();
230 foreach ( $opts as $key => $value ) {
231 \WP_CLI::log( sprintf( '%-22s %s', $key, is_scalar( $value ) ? (string) $value : wp_json_encode( $value ) ) );
232 }
233 return;
234 }
235
236 if ( 'set' === $action ) {
237 $patch = array();
238 $behaviors = array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE );
239
240 $has_context = isset( $assoc['for'] ) && '' !== $assoc['for'];
241 $has_behavior = isset( $assoc['behavior'] ) && '' !== $assoc['behavior'];
242
243 // --for and --behavior are a pair: both or neither.
244 if ( $has_context xor $has_behavior ) {
245 \WP_CLI::error( 'Pass --for and --behavior together.' );
246 return;
247 }
248 if ( $has_context && $has_behavior ) {
249 if ( ! in_array( $assoc['for'], self::CONTEXTS, true ) ) {
250 \WP_CLI::error( 'Invalid --for. Use one of: ' . implode( ', ', self::CONTEXTS ) . '.' );
251 return;
252 }
253 if ( ! in_array( $assoc['behavior'], $behaviors, true ) ) {
254 \WP_CLI::error( 'Invalid --behavior. Use one of: ' . implode( ', ', $behaviors ) . '.' );
255 return;
256 }
257 $patch[ 'behavior_' . $assoc['for'] ] = $assoc['behavior'];
258 }
259 if ( isset( $assoc['frequency'] ) ) {
260 $patch['frequency'] = (int) $assoc['frequency'];
261 }
262 if ( empty( $patch ) ) {
263 \WP_CLI::error( 'Nothing to set. Provide --for= --behavior= and/or --frequency=' );
264 return;
265 }
266 $this->update_settings( $patch );
267 \WP_CLI::success( 'Updated heartbeat settings.' );
268 return;
269 }
270
271 \WP_CLI::error( "Unknown action: $action" );
272 }
273 }
274