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 ]; } }