forecasts = $forecasts; $this->scheduler = $scheduler; $this->logger = $logger; } /** * Register action handlers and ensure the daily schedule exists. */ public function register_hooks(): void { add_action( self::DAILY_HOOK, array( $this, 'start' ) ); add_action( self::BATCH_HOOK, array( $this, 'run_batch' ) ); // Action Scheduler's data store isn't ready until `init`; scheduling any // earlier (we boot on plugins_loaded) silently fails with a notice. Wait // for Action Scheduler to announce it is ready. add_action( 'action_scheduler_init', array( $this, 'ensure_schedule' ) ); } /** * Ensure the recurring daily job is scheduled (idempotent). */ public function ensure_schedule(): void { $this->scheduler->schedule_recurring( time() + HOUR_IN_SECONDS, DAY_IN_SECONDS, self::DAILY_HOOK ); } /** * Begin a full recompute (resets the batch cursor and enqueues the first batch). */ public function start(): void { update_option( self::OFFSET_OPTION, 0, false ); $this->scheduler->enqueue_async( self::BATCH_HOOK ); } /** * Recompute the first batch synchronously (so a manual trigger reflects * immediately for typical catalogues) and chain the remainder in the * background. * * @return int Units processed synchronously. */ public function run_now(): int { $processed = $this->forecasts->recompute_batch( self::BATCH_SIZE, 0 ); if ( $processed >= self::BATCH_SIZE ) { update_option( self::OFFSET_OPTION, $processed, false ); $this->scheduler->enqueue_async( self::BATCH_HOOK ); } else { update_option( self::OFFSET_OPTION, 0, false ); } return $processed; } /** * Process one batch and chain the next until the catalogue is covered. */ public function run_batch(): void { $offset = (int) get_option( self::OFFSET_OPTION, 0 ); $processed = $this->forecasts->recompute_batch( self::BATCH_SIZE, $offset ); if ( $processed >= self::BATCH_SIZE ) { update_option( self::OFFSET_OPTION, $offset + $processed, false ); $this->scheduler->enqueue_async( self::BATCH_HOOK ); } else { update_option( self::OFFSET_OPTION, 0, false ); $this->logger->info( 'Forecast recompute complete.' ); } } }