<?php
|
namespace JVBase\rest\routes;
|
|
use Exception;
|
use JVBase\JVB;
|
use JVBase\managers\Cache;
|
use JVBase\rest\RestRouteManager;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use DateTime;
|
use DateTimeZone;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
class QueueRoutes extends RestRouteManager
|
{
|
public function __construct()
|
{
|
$this->cache_name = 'queue';
|
$this->cache_ttl = 300;
|
parent::__construct();
|
|
if (JVB_TESTING) {
|
$this->cache->flush();
|
}
|
}
|
|
/**
|
* Registers queue routes
|
* @return void
|
*/
|
public function registerRoutes():void
|
{
|
register_rest_route($this->namespace, '/queue', [
|
[
|
'methods' => 'GET',
|
'callback' => [$this, 'getQueue'],
|
'permission_callback' => [$this, 'checkPermission'],
|
'args' => [
|
'status' => [
|
'type' => 'string',
|
'enum' => ['all', 'queued', 'pending', 'processing', 'completed', 'failed', 'failed_permanent'],
|
'default' => 'all'
|
],
|
'ids' => [
|
'required' => false,
|
'type' => 'string',
|
'description' => 'Comma-separated list of operation IDs'
|
],
|
'limit' => [
|
'type' => 'integer',
|
'default' => 50,
|
'minimum' => 1,
|
'maximum' => 100
|
]
|
]
|
],
|
[
|
'methods' => 'POST',
|
'callback' => [$this, 'handleAction'],
|
'permission_callback' => [$this, 'checkPermission'],
|
'args' => [
|
'ids' => [
|
'required' => true,
|
'type' => 'array',
|
'items' => [
|
'type' => 'string'
|
],
|
'description' => 'Array of operation IDs (single or multiple)'
|
],
|
'action' => [
|
'required' => true,
|
'type' => 'string',
|
'enum' => ['dismiss', 'retry', 'cancel'],
|
'description' => 'Action to perform on the operations'
|
]
|
]
|
]
|
]);
|
}
|
|
/**
|
* Get queue operations with optional filtering
|
*
|
* @param WP_REST_Request $request
|
* @return WP_REST_Response
|
*/
|
public function getQueue(WP_REST_Request $request): WP_REST_Response
|
{
|
$user_id = $request->get_param('user');
|
$status = sanitize_text_field($request->get_param('status'));
|
$ids = $request->get_param('ids');
|
$limit = intval($request->get_param('limit'));
|
// Use base class user-specific header checking
|
$key = $this->cache->generateKey(['user'=> $user_id, 'status'=> $status, 'ids'=> $ids, 'limit'=> $limit]);
|
$cache_check = $this->checkHeaders($request, $key);
|
if ($cache_check) {
|
return $cache_check;
|
}
|
|
// Build filters for getUserOperations
|
$filters = [
|
'not_dismissed' => true,
|
'limit' => $limit ?: 50,
|
];
|
|
if ($status && $status !== 'all') {
|
$filters['state'] = $status;
|
}
|
|
if (!empty($ids)) {
|
$filters['ids'] = array_map('trim', explode(',', $ids));
|
}
|
|
// Get operations via Queue
|
$operations = JVB()->queue()->getUserOperations($user_id, $filters);
|
|
// Format operations for API response
|
$formatted = array_map([$this, 'formatOperationFromObject'], $operations);
|
|
$response = new WP_REST_Response([
|
'items' => $formatted,
|
'total' => count($formatted),
|
'timestamp' => date('c'),
|
'has_more' => count($formatted) === ($limit ?: 50),
|
'queue_stats' => $this->getQueueStats($user_id),
|
'server_time' => date('c')
|
]);
|
|
return $this->addCacheHeaders($response);
|
}
|
|
|
/**
|
* Get queue statistics for user
|
*/
|
protected function getQueueStats(int $user_id): array
|
{
|
$stats = JVB()->queue()->getUserStats($user_id);
|
|
// Add frontend-only statuses that don't exist in backend
|
return array_merge([
|
'queued' => 0,
|
'localProcessing' => 0,
|
'uploading' => 0,
|
], $stats);
|
}
|
|
/**
|
* Map backend state/outcome to frontend status
|
* Backend uses: state (pending, scheduled, processing, completed) + outcome (pending, success, partial, failed, failed_permanent)
|
* Frontend expects: queued, pending, processing, completed, failed, failed_permanent
|
*/
|
protected function mapStateToStatus(string $state, ?string $outcome): string
|
{
|
// If completed, check outcome for failure states
|
if ($state === 'completed') {
|
return match($outcome) {
|
'failed' => 'failed',
|
'failed_permanent' => 'failed_permanent',
|
'partial' => 'completed', // or could be 'partial' if JS supports it
|
default => 'completed'
|
};
|
}
|
|
// Map other states directly
|
return match($state) {
|
'scheduled' => 'pending',
|
default => $state
|
};
|
}
|
|
/**
|
* Format Operation object for API response
|
*/
|
protected function formatOperationFromObject(\JVBase\managers\queue\Operation $op): array
|
{
|
$formatted = [
|
'id' => $op->id,
|
'type' => $op->type,
|
'status' => $this->mapStateToStatus($op->state, $op->outcome),
|
'progress_count' => $op->processedItems,
|
'count' => $op->totalItems,
|
'retries' => $op->retries,
|
'data' => $op->requestData,
|
'result' => $op->result ?? [],
|
];
|
|
$formatted['created_at'] = $this->formatTimestamp($op->scheduledAt);
|
$formatted['updated_at'] = $this->formatTimestamp($op->completedAt ?? $op->startedAt ?? $op->scheduledAt);
|
|
if ($op->state === 'completed' && $op->completedAt) {
|
$formatted['completed_at'] = $this->formatTimestamp($op->completedAt);
|
}
|
|
if ($op->errorMessage) {
|
$formatted['error_message'] = $op->errorMessage;
|
}
|
|
if ($formatted['count'] > 0) {
|
$formatted['progress_percentage'] = round(
|
($formatted['progress_count'] / $formatted['count']) * 100
|
);
|
}
|
|
$formatted['title'] = $this->getOperationTitle($op->type, $op->requestData);
|
$formatted['user_dismissed'] = $op->userDismissed;
|
|
return $formatted;
|
}
|
|
/**
|
* Get human-readable operation title
|
*/
|
protected function getOperationTitle(string $type, array $data): string
|
{
|
$titles = [
|
'attach_upload_to_content' => 'Attaching Upload',
|
'content_update' => 'Updating Content',
|
'user_settings' => 'Updating Settings',
|
'bulk_operation' => 'Bulk Operation',
|
'image_processing' => 'Processing Images',
|
'notification_send' => 'Sending Notification',
|
'cache_clear' => 'Clearing Cache',
|
'data_export' => 'Exporting Data',
|
'data_import' => 'Importing Data'
|
];
|
|
$base_title = $titles[$type] ?? ucwords(str_replace('_', ' ', $type));
|
|
if ($type === 'attach_upload_to_content' && $data['mode'] === 'selection') {
|
$base_title .= '; Waiting on your Groupings to proceed...';
|
}
|
// Add context if available
|
if (!empty($data['content'])) {
|
$content_type = ucfirst($data['content']);
|
$base_title = str_replace('Content', $content_type, $base_title);
|
}
|
|
return $base_title;
|
}
|
|
/**
|
* Update operation status (dismiss or retry)
|
*
|
* @param WP_REST_Request $request
|
* @return WP_REST_Response
|
*/
|
public function handleAction(WP_REST_Request $request): WP_REST_Response
|
{
|
$data = $request->get_json_params();
|
$ids = $data['ids'] ?? [];
|
$action = $data['action'] ?? '';
|
$user_id = (int)$data['user'];
|
|
// Validate input
|
if (empty($ids) || !is_array($ids)) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'Missing or invalid operation IDs'
|
], 400);
|
}
|
|
if (!in_array($action, ['dismiss', 'retry', 'cancel'])) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'Invalid action. Must be: dismiss, retry, or cancel'
|
], 400);
|
}
|
|
// Get operations via Queue - verifies ownership
|
$operations = JVB()->queue()->getUserOperations($user_id, [
|
'ids' => $ids,
|
'limit' => count($ids),
|
]);
|
|
if (empty($operations)) {
|
return new WP_REST_Response([
|
'success' => false,
|
'message' => 'No valid operations found'
|
], 404);
|
}
|
|
$result = $this->processQueueAction($action, $operations, $user_id);
|
|
if ($result['success']) {
|
Cache::touch($user_id);
|
}
|
|
return new WP_REST_Response($result);
|
}
|
|
protected function processQueueAction(string $action, array $operations, int $user_id): array
|
{
|
$queue = JVB()->queue();
|
$processed_count = 0;
|
$errors = [];
|
$valid_ids = [];
|
|
foreach ($operations as $op) {
|
try {
|
$result = match($action) {
|
'dismiss' => $queue->dismiss($op->id),
|
'retry' => $queue->retry($op->id, $user_id),
|
'cancel' => $queue->cancel($op->id, $user_id),
|
default => false,
|
};
|
|
if ($result) {
|
$processed_count++;
|
$valid_ids[] = $op->id;
|
} else {
|
// Only add errors for meaningful failures
|
if ($action === 'retry' && ($op->state !== 'completed' || !in_array($op->outcome, ['failed', 'failed_permanent']))) {
|
$errors[] = "Operation {$op->id} cannot be retried (state: {$op->state}, outcome: {$op->outcome})";
|
}
|
// Silently skip cancel failures (can't cancel processing/completed)
|
}
|
} catch (Exception $e) {
|
$errors[] = "Error processing operation {$op->id}: " . $e->getMessage();
|
}
|
}
|
|
$message = $this->getActionMessage($action, $processed_count);
|
if (!empty($errors)) {
|
$message .= ". Errors: " . implode(', ', $errors);
|
}
|
|
return [
|
'success' => $processed_count > 0,
|
'action' => $action,
|
'processed_count' => $processed_count,
|
'total_requested' => count($operations),
|
'processed_ids' => $valid_ids,
|
'errors' => $errors,
|
'message' => $message
|
];
|
}
|
|
/**
|
* Get user-friendly message for action results
|
*/
|
protected function getActionMessage(string $action, int $count): string
|
{
|
if ($count === 0) {
|
return "No operations were {$action}ed";
|
}
|
|
$past_tense = [
|
'dismiss' => 'dismissed',
|
'retry' => 'retried',
|
'cancel' => 'cancelled'
|
];
|
|
return "{$count} operation" . ($count === 1 ? '' : 's') . " {$past_tense[$action]}";
|
}
|
|
public function getOperationErrors(WP_REST_Request $request): WP_REST_Response
|
{
|
$user_id = get_current_user_id();
|
|
$operations = JVB()->queue()->getUserOperations($user_id, [
|
'state' => 'completed',
|
'outcome' => ['failed', 'failed_permanent', 'partial'],
|
'has_errors' => true,
|
'order_by' => 'updated_at DESC',
|
'limit' => 20,
|
]);
|
|
$formatted = array_map(function($op) {
|
return [
|
'id' => $op->id,
|
'type' => $op->type,
|
'error_message' => $op->errorMessage,
|
'failed_items' => $op->failedItems ?? [],
|
'retries' => $op->retries,
|
'created_at' => $op->scheduledAt,
|
'updated_at' => $op->completedAt,
|
'error_details' => $this->parseErrorMessage($op->errorMessage ?? ''),
|
];
|
}, $operations);
|
|
return new WP_REST_Response([
|
'errors' => $formatted,
|
'total' => count($formatted)
|
]);
|
}
|
protected function parseErrorMessage(string $error_message): array
|
{
|
if (str_contains($error_message, ' | ')) {
|
$parts = explode(' | ', $error_message);
|
return [
|
'original_error' => $parts[0] ?? '',
|
'cleanup_reason' => $parts[1] ?? ''
|
];
|
}
|
|
return [
|
'original_error' => $error_message,
|
'cleanup_reason' => null
|
];
|
}
|
}
|