table_name = $wpdb->prefix . 'spinxqm_queue'; } /** * Whether the queue table exists. */ public function table_ready(): bool { global $wpdb; $like = $wpdb->esc_like( $this->table_name ); $found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $like ) ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder return ! empty( $found ); } /** * Upsert a row from the in-memory transient job (same job_id UUID). * * @param string $job_id Job ID (UUID) shared by the transient and the queue row. */ public function sync_from_transient( string $job_id ): void { if ( ! $this->table_ready() ) { return; } $job = Helpers::get_job( $job_id ); if ( empty( $job ) ) { return; } global $wpdb; $status = self::normalize_status( (string) ( $job['status'] ?? 'running' ) ); $now = current_time( 'mysql' ); $row = array( 'operation_type' => sanitize_key( (string) ( $job['type'] ?? '' ) ), 'status' => $status, 'priority' => 50, 'payload' => wp_json_encode( $job ), 'result' => isset( $job['result'] ) ? wp_json_encode( $job['result'] ) : null, 'error_message' => null, 'dependencies' => wp_json_encode( array() ), 'dependencies_satisfied' => 1, 'updated_at' => $now, ); if ( in_array( $status, array( 'failed', 'error' ), true ) ) { $row['error_message'] = (string) ( $job['message'] ?? '' ); } if ( in_array( $status, array( 'completed', 'failed', 'success', 'error', 'cancelled' ), true ) ) { $row['completed_at'] = $now; } $existing = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE job_id = %s', $this->table_name, $job_id ) ); if ( $existing ) { $wpdb->update( $this->table_name, $row, array( 'job_id' => $job_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return; } $row['job_id'] = $job_id; $row['created_at'] = $now; $row['started_at'] = $now; $wpdb->insert( $this->table_name, $row ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery } /** * Return the job_id of the most recent active (queued/running/paused) row, * or an empty string when nothing is in flight. */ public function find_active(): string { if ( ! $this->table_ready() ) { return ''; } global $wpdb; $job_id = $wpdb->get_var( $wpdb->prepare( "SELECT job_id FROM %i WHERE status IN ( 'queued', 'running', 'paused' ) ORDER BY created_at DESC LIMIT 1", $this->table_name ) ); return is_string( $job_id ) ? $job_id : ''; } /** * Count queue rows that are running or queued. * * @return array */ public function count_by_status(): array { if ( ! $this->table_ready() ) { return array( 'running' => 0, 'queued' => 0, ); } global $wpdb; $running = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE status = %s', $this->table_name, 'running' ) ); $queued = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE status = %s', $this->table_name, 'queued' ) ); return array( 'running' => $running, 'queued' => $queued, ); } /** * Fetch a filtered, paginated page of queue rows. * * @param array $filters Optional `type` and `status` filters. * @param int $page 1-based page number. * @param int $per_page Rows per page. * @return array */ public function list_jobs( array $filters, int $page, int $per_page ): array { if ( ! $this->table_ready() ) { return array( 'operations' => array(), 'pagination' => array( 'total' => 0, 'pages' => 0, 'current_page' => $page, 'per_page' => $per_page, ), ); } global $wpdb; // Sentinel pass-through guards keep the query a single literal string. $type = ! empty( $filters['type'] ) ? sanitize_key( (string) $filters['type'] ) : ''; $status = ! empty( $filters['status'] ) ? sanitize_key( (string) $filters['status'] ) : ''; $offset = max( 0, ( $page - 1 ) * $per_page ); $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE ( %s = '' OR operation_type = %s ) AND ( %s = '' OR status = %s )", $this->table_name, $type, $type, $status, $status ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT job_id, operation_type, status, error_message, created_at, completed_at, updated_at FROM %i WHERE ( %s = '' OR operation_type = %s ) AND ( %s = '' OR status = %s ) ORDER BY created_at DESC LIMIT %d OFFSET %d", $this->table_name, $type, $type, $status, $status, $per_page, $offset ), ARRAY_A ); return array( 'operations' => is_array( $rows ) ? $rows : array(), 'pagination' => array( 'total' => $total, 'pages' => $per_page > 0 ? (int) ceil( $total / $per_page ) : 0, 'current_page' => $page, 'per_page' => $per_page, ), ); } /** * Map a job status to a known queue status (unknown values become "running"). * * @param string $status Raw job status. * @return string */ private static function normalize_status( string $status ): string { $status = strtolower( $status ); if ( in_array( $status, array( 'queued', 'running', 'completed', 'failed', 'paused', 'success', 'error', 'cancelled' ), true ) ) { return $status; } if ( 'complete' === $status ) { return 'completed'; } return 'running'; } }