@@ -488,7 +488,11 @@ protected function fetch_stats( $args = array() ) {
488488 *
489489 * Unlike the above function, this caches data in the post meta table. As such,
490490 * it prevents wp_options from blowing up when retrieving views for large numbers
491- * of posts at the same time. However, the final response is the same as above.
491+ * of posts at the same time.
492+ *
493+ * This function returns valid arrays and WP_Error objects from cache if within the expiration period.
494+ * If the cached entry is malformed or invalid, a refresh is triggered regardless of cache time.
495+ * This self-healing behavior reduces API calls when remote fetch fails, but ensures data validity.
492496 *
493497 * @param array $args Query parameters.
494498 * @param int $post_id Post ID to acquire stats for.
@@ -503,34 +507,59 @@ protected function fetch_post_stats( $args, $post_id ) {
503507 if ( $ stats_cache ) {
504508 $ data = reset ( $ stats_cache );
505509
506- if (
507- ! is_array ( $ data )
508- || empty ( $ data )
509- || is_wp_error ( $ data )
510- ) {
511- return $ data ;
512- }
513-
514- $ time = key ( $ data );
515- $ views = $ data [ $ time ] ?? null ;
516-
517- // Bail if data is malformed.
518- if ( ! is_numeric ( $ time ) || ! is_array ( $ views ) ) {
519- return $ data ;
520- }
510+ // Check if we have a valid cache structure with a time key.
511+ if ( is_array ( $ data ) && ! empty ( $ data ) ) {
512+ $ time = key ( $ data );
521513
522- /** This filter is already documented in projects/packages/stats/src/class-wpcom-stats.php */
523- $ expiration = apply_filters (
524- 'jetpack_fetch_stats_cache_expiration ' ,
525- self ::STATS_CACHE_EXPIRATION_IN_MINUTES * MINUTE_IN_SECONDS
526- );
514+ // If we have a numeric time, check if cache is still valid.
515+ if ( is_numeric ( $ time ) ) {
516+ /** This filter is already documented in projects/packages/stats/src/class-wpcom-stats.php */
517+ $ expiration = apply_filters (
518+ 'jetpack_fetch_stats_cache_expiration ' ,
519+ self ::STATS_CACHE_EXPIRATION_IN_MINUTES * MINUTE_IN_SECONDS
520+ );
527521
528- if ( ( time () - $ time ) < $ expiration ) {
529- return array_merge ( array ( 'cached_at ' => $ time ), $ views );
522+ // If within cache period, return cached data after type validation.
523+ if ( ( time () - $ time ) < $ expiration ) {
524+ $ cached_value = $ data [ $ time ];
525+
526+ // If it's an array or WP_Error, handle appropriately.
527+ if ( is_wp_error ( $ cached_value ) ) {
528+ return $ cached_value ;
529+ }
530+ if ( is_array ( $ cached_value ) ) {
531+ return array_merge ( array ( 'cached_at ' => $ time ), $ cached_value );
532+ }
533+
534+ // For any other unexpected type, treat as malformed cache.
535+ // Fall through to refresh.
536+ }
537+ }
530538 }
531539 }
532540
541+ // Cache doesn't exist, is expired, or is malformed - refresh it.
542+ return $ this ->refresh_post_stats_cache ( $ endpoint , $ args , $ post_id , $ meta_name );
543+ }
544+
545+ /**
546+ * Force fetch stats from WPCOM, and always update cache.
547+ *
548+ * This function will cache the result regardless of whether the fetch succeeds
549+ * or fails. This ensures that failed requests are also cached, reducing the
550+ * frequency of API calls when the remote service is experiencing issues.
551+ *
552+ * @param string $endpoint The stats endpoint.
553+ * @param array $args The query arguments.
554+ * @param int $post_id The post ID.
555+ * @param string $meta_name The meta name.
556+ *
557+ * @return array|WP_Error
558+ */
559+ protected function refresh_post_stats_cache ( $ endpoint , $ args , $ post_id , $ meta_name ) {
533560 $ wpcom_stats = $ this ->fetch_remote_stats ( $ endpoint , $ args );
561+
562+ // Always cache the result, even if it's an error or empty.
534563 update_post_meta ( $ post_id , $ meta_name , array ( time () => $ wpcom_stats ) );
535564
536565 return $ wpcom_stats ;
0 commit comments