array(), 'kinds' => array(), 'products' => 0, 'capped' => false); if (!function_exists('as_get_scheduled_actions')) { return $queue; } $reads = array(ActionScheduler_Store::STATUS_RUNNING => 20, ActionScheduler_Store::STATUS_PENDING => 200); foreach ($reads as $status => $limit) { $actions = as_get_scheduled_actions(array( 'group' => RAYACHAT_SYNC_GROUP, 'status' => $status, 'per_page' => $limit, 'orderby' => 'date', 'order' => 'ASC', )); foreach ($actions as $id => $action) { $item = rayachat_queue_item($action, $status === ActionScheduler_Store::STATUS_RUNNING); if (!$item) { continue; } $item['id'] = (int) $id; $queue['items'][] = $item; $group = in_array($item['kind'], array('change', 'bulk'), true) ? 'change' : $item['kind']; $count = $item['kind'] === 'full' ? 1 : count($item['ids']); $queue['kinds'][$group] = ($queue['kinds'][$group] ?? 0) + $count; if ($item['kind'] !== 'image' && $item['kind'] !== 'full') { $queue['products'] += $count; } } $queue['capped'] = $queue['capped'] || count($actions) >= $limit; } return $queue; } /** * One job as the page describes it, or null for the daily check, which the * card above shows as its own date. 'change' is a single product waiting out * the delay; whether the owner's edit or a sale started it is settled only for * the lines drawn (rayachat_queue_settle). */ function rayachat_queue_item($action, $running) { $args = $action->get_args(); $schedule = $action->get_schedule(); $date = $schedule ? $schedule->get_date() : null; // An async job has no date: it runs the next time the queue does. $item = array('at' => $date ? $date->getTimestamp() : 0, 'running' => $running, 'ids' => array(), 'attempt' => 0); switch ($action->get_hook()) { case 'rayachat_sync_products': $item['ids'] = isset($args[0]) && is_array($args[0]) ? array_map('intval', $args[0]) : array(); $item['attempt'] = isset($args[1]) ? (int) $args[1] : 0; if ($item['attempt'] > 0) { $item['kind'] = 'retry'; } elseif (!$date) { $item['kind'] = 'later'; } else { $item['kind'] = count($item['ids']) > 1 ? 'bulk' : 'change'; } return $item['ids'] ? $item : null; case 'rayachat_sync_image': $item['kind'] = 'image'; $item['ids'] = array((int) ($args[0] ?? 0)); return $item; case 'rayachat_full_sync': $item['kind'] = 'full'; $item['phase'] = (string) ($args[0] ?? 'products'); $item['page'] = max(1, (int) ($args[1] ?? 1)); $item['attempt'] = (int) ($args[2] ?? 0); return $item; } return null; } /** * Loads what the drawn lines name in two reads, the products' posts and the * edit notes, and tells an edit from a sale: an edit's wait keeps a note * naming it (rayachat_queue_changes), a sale's has none. */ function rayachat_queue_settle(array $items) { $ids = array(); $notes = array(); foreach ($items as $item) { $ids = array_merge($ids, array_slice($item['ids'], 0, 1)); if ($item['kind'] === 'change') { $notes[] = '_transient_rayachat_wait_' . $item['ids'][0]; $notes[] = '_transient_timeout_rayachat_wait_' . $item['ids'][0]; } } if ($ids && function_exists('_prime_post_caches')) { _prime_post_caches(array_unique($ids), false, false); } if ($notes && !wp_using_ext_object_cache() && function_exists('wp_prime_option_caches')) { wp_prime_option_caches($notes); } foreach ($items as &$item) { // An async job's schedule has no date, but its row keeps when it was // queued, and it was due from then. if (!$item['at'] && !$item['running']) { try { $item['at'] = ActionScheduler::store()->get_date($item['id'])->getTimestamp(); } catch (Exception $e) { $item['at'] = 0; } } // Action Scheduler runs one batch at a time for every plugin on the // site, and only when a request starts it, so a job can sit past its // time while a cache preload or another plugin's jobs go first. $item['late'] = !$item['running'] && $item['at'] && time() - $item['at'] >= MINUTE_IN_SECONDS ? time() - $item['at'] : 0; if ($item['kind'] !== 'change') { continue; } $note = get_transient('rayachat_wait_' . $item['ids'][0]); $edit = isset($note['wait'], $note['first']) && (int) $note['wait'] === $item['id']; $item['kind'] = $edit ? 'edit' : 'sale'; if ($edit) { $item['cap'] = (int) $note['first'] + 2 * rayachat_sync_delay() * MINUTE_IN_SECONDS; } } unset($item); return $items; } // The products a line is about: a name and its edit link, or the first name and how many more. function rayachat_queue_subject(array $item) { if ($item['kind'] === 'full') { return esc_html__('Full sync', 'raya-ai'); } $id = $item['ids'][0]; $title = get_post($id) ? get_the_title($id) : ''; /* translators: %s: the product's ID. */ $name = $title !== '' ? $title : sprintf(__('Product #%s', 'raya-ai'), rayachat_digits($id)); $link = get_post($id) ? get_edit_post_link($id) : ''; $html = $link ? sprintf('%s', esc_url($link), esc_html($name)) : esc_html($name); $more = count($item['ids']) - 1; if ($more < 1) { return $html; } return sprintf( /* translators: 1: a product's name, 2: how many other products. */ esc_html(_n('%1$s and %2$s more product', '%1$s and %2$s more products', $more, 'raya-ai')), $html, esc_html(rayachat_number($more)) ); } // Why a line waits, in a sentence. A single product no longer published is about to leave the chatbot. function rayachat_queue_reason(array $item) { if (count($item['ids']) === 1 && $item['kind'] !== 'image') { $status = get_post_status($item['ids'][0]); if ($status === false) { return __('Deleted from the store, so it will be removed from the chatbot.', 'raya-ai'); } if ($status === 'trash') { return __('In the trash, so it will be removed from the chatbot. Restore it before then to keep it.', 'raya-ai'); } if ($status !== 'publish') { return __('No longer published, so it will be removed from the chatbot.', 'raya-ai'); } } switch ($item['kind']) { case 'edit': return sprintf( /* translators: %s: a time of day. */ __('Your edit. Each new edit starts the wait again, up to %s at the latest.', 'raya-ai'), rayachat_digits(wp_date(get_option('time_format'), $item['cap'])) ); case 'sale': return __('A sale or stock update. It starts no wait over, and however many sales, it is sent once.', 'raya-ai'); case 'bulk': return __('A bulk edit or stock import, sent together after the wait.', 'raya-ai'); case 'retry': /* translators: 1: this retry's number, 2: how many retries there are in all. */ return sprintf(__('Did not get through last time. Retry %1$s of %2$s, each later than the last.', 'raya-ai'), rayachat_number($item['attempt']), rayachat_number(RAYACHAT_RETRY_LIMIT)); case 'later': return __('Left over from a full sync that ran out of time; sent in the background.', 'raya-ai'); case 'image': return __('The main picture, uploaded on its own after the product’s details.', 'raya-ai'); } $step = $item['phase'] === 'reconcile' ? __('Removing the products the store no longer shows.', 'raya-ai') /* translators: 1: first product number, 2: last product number. */ : sprintf(__('Checking products %1$s to %2$s against the chatbot.', 'raya-ai'), rayachat_number(($item['page'] - 1) * RAYACHAT_CHECK_PAGE + 1), rayachat_number($item['page'] * RAYACHAT_CHECK_PAGE)); return $item['attempt'] > 0 ? $step . ' ' . __('Trying again: Rayachat could not be reached.', 'raya-ai') : $step; } function rayachat_queue_when(array $item) { if ($item['running']) { return __('Sending now', 'raya-ai'); } if ($item['late'] >= HOUR_IN_SECONDS) { $hours = (int) floor($item['late'] / HOUR_IN_SECONDS); /* translators: %s: number of hours. */ return sprintf(_n('Waiting for WordPress, %s hour late', 'Waiting for WordPress, %s hours late', $hours, 'raya-ai'), rayachat_number($hours)); } if ($item['late']) { $minutes = (int) floor($item['late'] / MINUTE_IN_SECONDS); /* translators: %s: number of minutes. */ return sprintf(_n('Waiting for WordPress, %s minute late', 'Waiting for WordPress, %s minutes late', $minutes, 'raya-ai'), rayachat_number($minutes)); } $left = $item['at'] - time(); if ($left < 60) { return __('Any moment', 'raya-ai'); } if ($left < HOUR_IN_SECONDS) { $minutes = (int) ceil($left / MINUTE_IN_SECONDS); /* translators: %s: number of minutes. */ return sprintf(_n('in %s minute', 'in %s minutes', $minutes, 'raya-ai'), rayachat_number($minutes)); } $hours = (int) round($left / HOUR_IN_SECONDS); /* translators: %s: number of hours. */ return sprintf(_n('in %s hour', 'in %s hours', $hours, 'raya-ai'), rayachat_number($hours)); } function rayachat_queue_icon(array $item) { if (count($item['ids']) === 1 && $item['kind'] !== 'image' && get_post_status($item['ids'][0]) !== 'publish') { return array('remove', 'trash'); } $icons = array( 'edit' => 'edit', 'sale' => 'cart', 'bulk' => 'screenoptions', 'retry' => 'controls-repeat', 'later' => 'update', 'image' => 'format-image', 'full' => 'database', ); return array($item['kind'], $icons[$item['kind']] ?? 'clock'); } function rayachat_render_sync_queue() { $queue = rayachat_sync_queue(); $items = rayachat_queue_settle(array_slice($queue['items'], 0, RAYACHAT_QUEUE_SHOWN)); $more = count($queue['items']) - count($items); $chips = array( /* translators: %s: number of products. */ 'change' => _n('%s product change', '%s product changes', $queue['kinds']['change'] ?? 0, 'raya-ai'), /* translators: %s: number of products. */ 'retry' => _n('%s to retry', '%s to retry', $queue['kinds']['retry'] ?? 0, 'raya-ai'), /* translators: %s: number of products. */ 'later' => _n('%s from a full sync', '%s from a full sync', $queue['kinds']['later'] ?? 0, 'raya-ai'), /* translators: %s: number of pictures. */ 'image' => _n('%s picture', '%s pictures', $queue['kinds']['image'] ?? 0, 'raya-ai'), 'full' => __('Full sync under way', 'raya-ai'), ); $jobs = count($queue['items']); ?>