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.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.4, at includes/modules/Preloader/PreloaderModule.php

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