`. public const BEHAVIOR_KEEP = 'keep'; // leave alone public const BEHAVIOR_THROTTLE = 'throttle'; // apply our `frequency` public const BEHAVIOR_DISABLE = 'disable'; // turn off entirely private const CONTEXTS = array( 'dashboard', 'editor', 'frontend' ); public function ui_metadata(): array { return array( 'label' => 'Heartbeat', 'icon' => 'Activity', 'description' => 'Control the WordPress Heartbeat API per context.', ); } /** * Default profile mirrors the IMPLEMENTATION recommendation: * - frontend: off by default (most polling-heavy, rarely needed) * - editor: throttled to 60s (was 15s) * - dashboard: throttled to 60s (was 60s — left as is) */ public function settings_schema(): array { $behavior_options = array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE ); $behavior_option_labels = array( self::BEHAVIOR_KEEP => 'Keep', self::BEHAVIOR_THROTTLE => 'Throttle', self::BEHAVIOR_DISABLE => 'Disable', ); return array( 'behavior_dashboard' => array( 'type' => 'enum', 'default' => self::BEHAVIOR_THROTTLE, 'options' => $behavior_options, 'option_labels' => $behavior_option_labels, 'label' => 'Dashboard', 'description' => 'Heartbeat behavior on /wp-admin/ screens (autosave, notifications).', ), 'behavior_editor' => array( 'type' => 'enum', 'default' => self::BEHAVIOR_THROTTLE, 'options' => $behavior_options, 'option_labels' => $behavior_option_labels, 'label' => 'Editor', 'description' => 'Heartbeat in the post / block editor. Disable only if you do not need autosave or co-edit locks.', ), 'behavior_frontend' => array( 'type' => 'enum', 'default' => self::BEHAVIOR_DISABLE, 'options' => $behavior_options, 'option_labels' => $behavior_option_labels, 'label' => 'Frontend', 'description' => 'Heartbeat on the public site. Recommended off — most themes never need it and it costs admin-ajax requests per visitor.', ), 'frequency' => array( 'type' => 'int', 'default' => 60, 'min' => 15, 'max' => 300, 'label' => 'Throttle Frequency', 'description' => 'Interval in seconds for contexts set to Throttle. 60 is a sane default; lower = faster sync but more requests.', ), ); } // rest_routes — using the Module base-class default (GET + POST under // /xspeed/v1/heartbeat/ wired to rest_get_settings + rest_update_settings). public function cli_commands(): array { return array( array( 'name' => 'xspeed heartbeat', 'callback' => array( $this, 'cli_handler' ), 'shortdesc' => 'Inspect or modify xSpeed heartbeat settings.', 'synopsis' => array( array( 'type' => 'positional', 'name' => 'action', 'options' => array( 'show', 'set' ), 'optional' => false, ), array( 'type' => 'assoc', 'name' => 'context', 'optional' => true, 'options' => self::CONTEXTS, ), array( 'type' => 'assoc', 'name' => 'behavior', 'optional' => true, 'options' => array( self::BEHAVIOR_KEEP, self::BEHAVIOR_THROTTLE, self::BEHAVIOR_DISABLE ), ), array( 'type' => 'assoc', 'name' => 'frequency', 'optional' => true, ), ), ), ); } public function boot(): void { // `init` is early enough to register our filters before Heartbeat // itself enqueues. Stay low priority to defer to plugins that ran // at plugins_loaded. add_action( 'init', array( $this, 'apply_settings' ), 5 ); } /** * Hook handler — translates our settings into Heartbeat behavior. Runs * on every request. The branches below are pure read + dispatch — no * I/O so the cost is negligible even on cached requests. */ public function apply_settings(): void { $settings = $this->get_settings(); $context = $this->detect_context(); $behavior = $settings[ 'behavior_' . $context ] ?? self::BEHAVIOR_KEEP; if ( self::BEHAVIOR_DISABLE === $behavior ) { // Dequeue the heartbeat script entirely. wp_deregister_script // runs on `init` priority 5 → before wp_default_scripts // (priority 10) re-registers, so we hook the actual enqueue // stage instead. add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_heartbeat' ), 1 ); add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_heartbeat' ), 1 ); } elseif ( self::BEHAVIOR_THROTTLE === $behavior ) { $interval = max( 15, min( 300, (int) ( $settings['frequency'] ?? 60 ) ) ); add_filter( 'heartbeat_settings', static function ( $hb_settings ) use ( $interval ) { $hb_settings['interval'] = $interval; return $hb_settings; } ); } // BEHAVIOR_KEEP → no filter, WP defaults apply. } /** * Dequeue + deregister the heartbeat script. Safe to call multiple * times — both wp functions are idempotent. */ public function dequeue_heartbeat(): void { wp_dequeue_script( 'heartbeat' ); wp_deregister_script( 'heartbeat' ); } /** * Classify the current request into dashboard / editor / frontend. * Editor here means the block / classic post editor screens — they're * the heaviest heartbeat consumer and worth their own bucket. */ private function detect_context(): string { if ( ! is_admin() ) { return 'frontend'; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( $screen && in_array( $screen->base, array( 'post', 'post-new' ), true ) ) { return 'editor'; } return 'dashboard'; } // REST handlers — provided by Module base class // (rest_get_settings / rest_update_settings). public function cli_handler( array $args, array $assoc ): void { $action = $args[0] ?? 'show'; if ( 'show' === $action ) { $opts = $this->get_settings(); foreach ( $opts as $key => $value ) { \WP_CLI::log( sprintf( '%-22s %s', $key, is_scalar( $value ) ? (string) $value : wp_json_encode( $value ) ) ); } return; } if ( 'set' === $action ) { $patch = array(); if ( isset( $assoc['context'], $assoc['behavior'] ) ) { $patch[ 'behavior_' . $assoc['context'] ] = $assoc['behavior']; } if ( isset( $assoc['frequency'] ) ) { $patch['frequency'] = (int) $assoc['frequency']; } if ( empty( $patch ) ) { \WP_CLI::error( 'Nothing to set. Provide --context= --behavior= and/or --frequency=' ); return; } $this->update_settings( $patch ); \WP_CLI::success( 'Updated heartbeat settings.' ); return; } \WP_CLI::error( "Unknown action: $action" ); } }