PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.7
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 1.2.0 All 28 releases
xspeed / includes / modules / Preloader / PreloaderModule.php

PreloaderModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.7, at includes/modules/Preloader/PreloaderModule.php

296 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Cache Preloader module.
4 *
5 * Owns the preloader's settings + the dashboard custom panel that pairs
6 * the schema controls with a Start/Stop/Status surface.
7 *
8 * Tier: Free (LiteSpeed parity — their crawler is free).
9 * Roadmap: §3 P3.1.
10 *
11 * @package XSpeed
12 */
13
14 declare(strict_types=1);
15
16 namespace XSpeed\Modules\Preloader;
17
18 defined( 'ABSPATH' ) || exit;
19
20 use XSpeed\Module;
21 use XSpeed\Preloader;
22 use XSpeed\Settings_Manager;
23
24 final class PreloaderModule extends Module {
25
26 public const SLUG = 'preloader';
27 public const TIER = self::TIER_FREE;
28 public const VERSION = '1.0.0';
29
30 public function ui_metadata(): array {
31 return array(
32 'label' => 'Preloader',
33 'icon' => 'Wand2',
34 'description' => 'Crawl the sitemap to warm cache so visitors never hit a cold MISS.',
35 // Custom panel wraps the schema-driven settings with a
36 // Start/Stop control surface + a live status readout
37 // (queue depth, last URL, recent errors).
38 'custom_panel' => 'PreloaderPanel',
39 );
40 }
41
42 public function settings_schema(): array {
43 return array(
44 'enabled' => array(
45 'type' => 'bool',
46 'default' => false,
47 'label' => 'Enable Preloader',
48 'description' => 'When on, xSpeed crawls the sitemap on the schedule below and warms the page cache.',
49 ),
50 'schedule' => array(
51 'type' => 'enum',
52 'default' => 'manual',
53 'options' => array( 'manual', 'hourly', 'daily', 'weekly' ),
54 'option_labels' => array(
55 'manual' => 'Manual',
56 'hourly' => 'Hourly',
57 'daily' => 'Daily',
58 'weekly' => 'Weekly',
59 ),
60 'label' => 'Schedule',
61 'description' => 'How often to start a fresh crawl. Manual means you trigger it from the dashboard.',
62 ),
63 'batch_size' => array(
64 'type' => 'int',
65 'default' => 5,
66 'min' => 1,
67 'max' => 50,
68 'label' => 'Batch Size',
69 'description' => 'URLs warmed per cron tick. Higher = faster crawl, more load on the origin.',
70 ),
71 'sitemap_url' => array(
72 'type' => 'string',
73 'default' => '',
74 'label' => 'Sitemap URL (optional)',
75 'description' => 'Override the auto-detected WordPress core sitemap (/wp-sitemap.xml). Leave blank for default.',
76 ),
77 'warm_on_publish' => array(
78 'type' => 'bool',
79 'default' => true,
80 'label' => 'Warm new content immediately',
81 'description' => 'When a post or page is published, fetch it once so the first visitor sees a cache HIT, not a cold MISS.',
82 ),
83 'warm_on_comment' => array(
84 'type' => 'bool',
85 'default' => false,
86 'label' => 'Re-warm after comments',
87 'description' => 'Re-warm a page after a comment is posted (Cache purges the page on comment; this fetches it back into cache).',
88 ),
89 );
90 }
91
92 public function rest_routes(): array {
93 // Module base gives us GET + POST for settings under
94 // /xspeed/v1/preloader/. We add Start/Stop/Status alongside.
95 $default = parent::rest_routes();
96 return array_merge(
97 $default,
98 array(
99 array(
100 'path' => '/start',
101 'methods' => 'POST',
102 'callback' => array( $this, 'rest_start' ),
103 ),
104 array(
105 'path' => '/stop',
106 'methods' => 'POST',
107 'callback' => array( $this, 'rest_stop' ),
108 ),
109 array(
110 'path' => '/status',
111 'methods' => 'GET',
112 'callback' => array( $this, 'rest_status' ),
113 ),
114 )
115 );
116 }
117
118 public function cli_commands(): array {
119 return array(
120 array(
121 'name' => 'xspeed preloader',
122 'callback' => array( $this, 'cli_handler' ),
123 'shortdesc' => 'Drive the cache preloader (start | stop | status).',
124 'synopsis' => array(
125 array(
126 'type' => 'positional',
127 'name' => 'action',
128 'options' => array( 'start', 'stop', 'status' ),
129 'optional' => false,
130 ),
131 ),
132 ),
133 );
134 }
135
136 public function boot(): void {
137 add_action( Preloader::CRON_HOOK, array( Preloader::class, 'tick' ) );
138 add_action( 'xspeed_preloader_recurring', array( Preloader::class, 'recurring_kickoff' ) );
139
140 // Apply schedule changes immediately whenever this module's
141 // settings get written (the standard per-module option hook).
142 add_action( 'update_option_xspeed_module_preloader', array( $this, 'on_settings_change' ), 10, 2 );
143 add_action( 'add_option_xspeed_module_preloader', array( $this, 'on_settings_added' ), 10, 2 );
144
145 // Content warmer — auto-warm a single URL on post publish /
146 // comment so the first visitor after a publish/comment sees a
147 // HIT, not the cold MISS that Cache::purge_all just created.
148 $opts = Settings_Manager::get( self::SLUG );
149 if ( ! empty( $opts['warm_on_publish'] ) ) {
150 add_action( 'transition_post_status', array( $this, 'on_post_transition' ), 10, 3 );
151 }
152 if ( ! empty( $opts['warm_on_comment'] ) ) {
153 add_action( 'comment_post', array( $this, 'on_comment_post' ), 20, 2 );
154 }
155 }
156
157 /**
158 * Hook: a post transitioned to publish. Warm its permalink once on
159 * shutdown so the post-save request itself stays fast.
160 *
161 * @param string $new New post status.
162 * @param string $old Old post status.
163 * @param \WP_Post $post Post object.
164 */
165 public function on_post_transition( $new, $old, $post ): void {
166 if ( 'publish' !== $new || 'publish' === $old ) {
167 return;
168 }
169 // Only warm public post types so we don't crawl private CPTs.
170 $post_type_obj = get_post_type_object( $post->post_type );
171 if ( ! $post_type_obj || empty( $post_type_obj->public ) ) {
172 return;
173 }
174 $url = get_permalink( $post );
175 if ( ! $url ) {
176 return;
177 }
178 // Defer to shutdown so the user's "Publish" click returns fast.
179 // (Cache::purge_all has already fired by then on the save_post
180 // hook, so the warm fetch lands AFTER the purge.)
181 add_action(
182 'shutdown',
183 static function () use ( $url ) {
184 Preloader::warm_one( $url, 'post published' );
185 },
186 20
187 );
188 }
189
190 /**
191 * Hook: comment posted. Warm the post's permalink so the page is
192 * back in cache before the next visitor lands.
193 *
194 * @param int $comment_id The comment ID.
195 * @param int $approved 1, 0, or 'spam'.
196 */
197 public function on_comment_post( $comment_id, $approved ): void {
198 // Approved comments only — pending/spam don't show on the
199 // public page and shouldn't trigger a warm.
200 if ( 1 !== (int) $approved ) {
201 return;
202 }
203 $comment = get_comment( $comment_id );
204 if ( ! $comment || ! $comment->comment_post_ID ) {
205 return;
206 }
207 $url = get_permalink( (int) $comment->comment_post_ID );
208 if ( ! $url ) {
209 return;
210 }
211 add_action(
212 'shutdown',
213 static function () use ( $url ) {
214 Preloader::warm_one( $url, 'comment posted' );
215 },
216 20
217 );
218 }
219
220 public function deactivate(): void {
221 wp_clear_scheduled_hook( Preloader::CRON_HOOK );
222 wp_clear_scheduled_hook( 'xspeed_preloader_recurring' );
223 }
224
225 public function on_settings_change( $old, $new ): void {
226 $enabled = is_array( $new ) && ! empty( $new['enabled'] );
227 $schedule = is_array( $new ) ? (string) ( $new['schedule'] ?? 'manual' ) : 'manual';
228 Preloader::apply_schedule( $enabled ? $schedule : 'manual' );
229 }
230
231 public function on_settings_added( $name, $value ): void {
232 $enabled = is_array( $value ) && ! empty( $value['enabled'] );
233 $schedule = is_array( $value ) ? (string) ( $value['schedule'] ?? 'manual' ) : 'manual';
234 Preloader::apply_schedule( $enabled ? $schedule : 'manual' );
235 }
236
237 public function rest_start( \WP_REST_Request $request ) {
238 $opts = Settings_Manager::get( self::SLUG );
239 if ( empty( $opts['enabled'] ) ) {
240 return new \WP_Error(
241 'xspeed_preloader_disabled',
242 __( 'Enable the preloader before starting a crawl.', 'xspeed' ),
243 array( 'status' => 409 )
244 );
245 }
246 return rest_ensure_response( Preloader::start() );
247 }
248
249 public function rest_stop( \WP_REST_Request $request ) {
250 return rest_ensure_response( Preloader::stop() );
251 }
252
253 public function rest_status( \WP_REST_Request $request ) {
254 return rest_ensure_response( Preloader::status() );
255 }
256
257 public function cli_handler( array $args, array $assoc ): void {
258 $action = $args[0] ?? 'status';
259
260 switch ( $action ) {
261 case 'start':
262 $opts = Settings_Manager::get( self::SLUG );
263 if ( empty( $opts['enabled'] ) ) {
264 \WP_CLI::error( 'Preloader is disabled. Enable it via wp xspeed preloader set --enabled=1 first.' );
265 return;
266 }
267 $state = Preloader::start();
268 \WP_CLI::success( sprintf( 'Queued %d URL%s.', $state['total'], 1 === $state['total'] ? '' : 's' ) );
269 return;
270
271 case 'stop':
272 Preloader::stop();
273 \WP_CLI::success( 'Preloader stopped.' );
274 return;
275
276 case 'status':
277 $state = Preloader::status();
278 \WP_CLI::log( 'Running : ' . ( $state['running'] ? 'yes' : 'no' ) );
279 \WP_CLI::log( 'Processed : ' . $state['processed'] . ' / ' . $state['total'] );
280 if ( $state['last_url'] ) {
281 \WP_CLI::log( 'Last URL : ' . $state['last_url'] );
282 }
283 if ( ! empty( $state['errors'] ) ) {
284 \WP_CLI::log( 'Errors : ' . count( $state['errors'] ) );
285 foreach ( array_slice( $state['errors'], -5 ) as $e ) {
286 \WP_CLI::log( ' ' . $e['url'] . '' . $e['error'] );
287 }
288 }
289 return;
290
291 default:
292 \WP_CLI::error( "Unknown action: $action" );
293 }
294 }
295 }
296