* @author Maximum Software Inc * @license GPL-3.0-only * @link https://agenistrator.com/ * * SPDX-FileCopyrightText: 2025-2026 Alex Ruban * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc * SPDX-License-Identifier: GPL-3.0-only */ namespace Agenistrator; use Agenistrator\Events\Progress_Event; // Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Progress_Tracker class. */ class Progress_Tracker { /** * Operation identifier (e.g., 'embeddings'). * * @var string */ private $operation; /** * Unique instance identifier for this operation run. * * @var string */ private $instance_id; /** * Private constructor - use static factory methods. * * @param string $operation The operation identifier. * @param string $instance_id The unique instance ID. */ private function __construct( $operation, $instance_id ) { $this->operation = $operation; $this->instance_id = $instance_id; } /** * Start a new operation. * * Generates a unique instance ID and pushes an initial progress event. * * @param string $operation The operation identifier (e.g., 'embeddings'). * @return self * @since 1.0.0 */ public static function start( $operation ) { $instance_id = uniqid( '', true ); $tracker = new self( $operation, $instance_id ); $tracker->update( 0, 0 ); return $tracker; } /** * Get currently running operations. * * Returns all running instances, allowing multiple concurrent operations * of the same type. * * @param string|null $operation Optional. Filter by operation type. * @return self[] Array of Progress_Tracker instances for running operations. * @since 1.0.0 */ public static function get_running( $operation = null ) { $events = Event_Queue::get_instance()->get_events_by_type( 'progress' ); // Track cancelled/completed operations and latest event per instance $cancelled = array(); $latest_by_instance = array(); foreach ( $events as $event ) { $instance_id = $event->get_instance_id(); if ( ! $instance_id ) { continue; } // Track any instance that has been cancelled or completed if ( $event->get_status() !== Progress_Event::STATUS_RUNNING ) { $cancelled[ $instance_id ] = true; } // Keep only the latest event per instance if ( ! isset( $latest_by_instance[ $instance_id ] ) ) { $latest_by_instance[ $instance_id ] = $event; } } // Remove cancelled/completed operations from the list $latest_by_instance = array_diff_key( $latest_by_instance, $cancelled ); // Create tracker instances for remaining running operations $trackers = array(); foreach ( $latest_by_instance as $instance_id => $event ) { $op = $event->get_operation(); if ( null !== $operation && $op !== $operation ) { continue; } $trackers[] = new self( $op, $instance_id ); } return $trackers; } /** * Get a running tracker by its instance ID. * * @param string $instance_id The instance ID to look up. * @return self|null The tracker if found and running, null otherwise. * @since 1.0.0 */ public static function get_by_instance_id( $instance_id ) { $events = Event_Queue::get_instance()->get_events_by_type( 'progress' ); $latest_event = null; foreach ( $events as $event ) { if ( $event->get_instance_id() !== $instance_id ) { continue; } // If this instance has been cancelled/completed at any point then it is not running if ( $event->get_status() !== Progress_Event::STATUS_RUNNING ) { return null; } // Keep the first (latest) event for this instance if ( null === $latest_event ) { $latest_event = $event; } } // If no events found, return null if ( null === $latest_event ) { return null; } return new self( $latest_event->get_operation(), $instance_id ); } /** * Get the operation identifier. * * @return string * @since 1.0.0 */ public function get_operation() { return $this->operation; } /** * Get the instance ID. * * @return string * @since 1.0.0 */ public function get_instance_id() { return $this->instance_id; } /** * Get the latest event for this tracker's operation and instance. * * @return Progress_Event|null Event or null if not found. */ private function get_latest_event() { $events = Event_Queue::get_instance()->get_events_by_type( 'progress' ); foreach ( $events as $event ) { if ( $event->get_operation() === $this->operation && $event->get_instance_id() === $this->instance_id ) { return $event; } } return null; } /** * Get current progress value. * * @return int * @since 1.0.0 */ public function get_current() { $event = $this->get_latest_event(); return $event ? $event->get_current() : 0; } /** * Get total items count. * * @return int * @since 1.0.0 */ public function get_total() { $event = $this->get_latest_event(); return $event ? $event->get_total() : 0; } /** * Get operation status. * * @return string * @since 1.0.0 */ public function get_status() { $event = $this->get_latest_event(); return $event ? $event->get_status() : Progress_Event::STATUS_RUNNING; } /** * Get created timestamp. * * @return int * @since 1.0.0 */ public function get_created_at() { $event = $this->get_latest_event(); return $event ? (int) $event->get_created_at() : 0; } /** * Get percent complete. * * @return float Percentage (0-100). * @since 1.0.0 */ public function get_percent_complete() { $event = $this->get_latest_event(); if ( ! $event ) { return 0.0; } $total = $event->get_total(); if ( $total <= 0 ) { return 0.0; } return round( ( $event->get_current() / $total ) * 100, 1 ); } /** * Update progress. * * @param int $current Current progress value. * @param int $total Total items to process. * @param string $status Operation status. * @since 1.0.0 */ public function update( $current, $total, $status = Progress_Event::STATUS_RUNNING ) { $event = new Progress_Event( $this->operation, $current, $total, $status, $this->instance_id ); $event->push(); } /** * Mark operation as complete. * * @param int $total Total items processed. * @since 1.0.0 */ public function complete( $total ) { $this->update( $total, $total, Progress_Event::STATUS_COMPLETED ); } /** * Cancel this operation. * @since 1.0.0 */ public function cancel() { $event = new Progress_Event( $this->operation, 0, 0, Progress_Event::STATUS_CANCELLED, $this->instance_id ); $event->push(); } /** * Check if this operation should stop. * * Returns true if: * - The operation was explicitly cancelled (cancelled event with our instance_id), or * - The operation was superseded by a newer instance (a running event with a different instance_id). * * This method scans all recent events to find cancel events, since a cancel event * may be "buried" by subsequent update events due to race conditions. * * @return bool * @since 1.0.0 */ public function is_cancelled() { $events = Event_Queue::get_instance()->get_events_by_type( 'progress' ); foreach ( $events as $event ) { if ( $event->get_operation() !== $this->operation ) { continue; } $status = $event->get_status(); $instance_id = $event->get_instance_id(); // Explicit cancellation of our instance (scan all events, don't stop early) if ( Progress_Event::STATUS_CANCELLED === $status && $instance_id === $this->instance_id ) { return true; } } return false; } }