| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Classifies the outcome of an admin view row read (getRedirectsForView). |
| 9 |
* |
| 10 |
* A row read can return an empty array for three structurally different |
| 11 |
* reasons, and the admin UI must treat them differently: |
| 12 |
* |
| 13 |
* - COMPLETE the source genuinely has no rows for this page; "No |
| 14 |
* records" is the truthful thing to render. |
| 15 |
* - PENDING the staged view_done build is not serveable yet. |
| 16 |
* - ERRORED the staged read threw. |
| 17 |
* - STALE_EMPTY the served snapshot returned zero rows while the live |
| 18 |
* source count says rows exist for the requested page -- |
| 19 |
* the "count shows thousands but the table is empty" |
| 20 |
* release blocker (i455). |
| 21 |
* |
| 22 |
* Only COMPLETE is trustworthy; the other three are "incomplete" and the |
| 23 |
* renderer shows a still-preparing state while the AJAX endpoint re-engages |
| 24 |
* the view-build poller. This class owns that small state machine plus the |
| 25 |
* pure stale-empty heuristic so the decision lives in one independently |
| 26 |
* testable place rather than spread across the read coordinator. |
| 27 |
*/ |
| 28 |
class ABJ_404_Solution_ViewReadOutcome { |
| 29 |
|
| 30 |
/** A read outcome has not been recorded yet this request. */ |
| 31 |
const STATUS_UNKNOWN = 'unknown'; |
| 32 |
/** The rows returned are a trustworthy listing (possibly a genuine empty set). */ |
| 33 |
const STATUS_COMPLETE = 'complete'; |
| 34 |
/** The staged view_done build is not serveable yet; rows could not be read. */ |
| 35 |
const STATUS_PENDING = 'pending'; |
| 36 |
/** The staged read threw; rows could not be read. */ |
| 37 |
const STATUS_ERRORED = 'errored'; |
| 38 |
/** Empty rows while the live source count says rows exist for this page. */ |
| 39 |
const STATUS_STALE_EMPTY = 'stale_empty'; |
| 40 |
|
| 41 |
/** @var string One of the STATUS_* constants. */ |
| 42 |
private $status = self::STATUS_UNKNOWN; |
| 43 |
|
| 44 |
/** @return void */ |
| 45 |
public function markPending(): void { |
| 46 |
$this->status = self::STATUS_PENDING; |
| 47 |
} |
| 48 |
|
| 49 |
/** @return void */ |
| 50 |
public function markErrored(): void { |
| 51 |
$this->status = self::STATUS_ERRORED; |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* Classify a successful (non-throwing) row read. A non-empty result is |
| 56 |
* always trustworthy; an empty result is trustworthy only when the live |
| 57 |
* source count agrees there are no rows for the requested page. The count |
| 58 |
* is probed (via $liveCountProbe, expected to return -1 when unavailable) |
| 59 |
* only when the rows are empty. Strict-mode reads |
| 60 |
* (`_abj404_throw_on_view_query_error`) classify pending/error themselves |
| 61 |
* and are never probed here. |
| 62 |
* |
| 63 |
* @param array<int|string, mixed> $rows |
| 64 |
* @param array<string, mixed> $tableOptions |
| 65 |
* @param callable():int $liveCountProbe |
| 66 |
* @return void |
| 67 |
*/ |
| 68 |
public function classifyRows(array $rows, array $tableOptions, callable $liveCountProbe): void { |
| 69 |
if (!empty($rows) || !empty($tableOptions['_abj404_throw_on_view_query_error'])) { |
| 70 |
// Non-empty rows are trustworthy; strict-mode empties are the |
| 71 |
// caller's own (already-classified) pending/error responsibility. |
| 72 |
$this->status = self::STATUS_COMPLETE; |
| 73 |
return; |
| 74 |
} |
| 75 |
$this->status = self::emptyRowsAreStale((int)$liveCountProbe(), $tableOptions) |
| 76 |
? self::STATUS_STALE_EMPTY |
| 77 |
: self::STATUS_COMPLETE; |
| 78 |
} |
| 79 |
|
| 80 |
/** |
| 81 |
* Pure heuristic: do empty rows contradict the live source count? True |
| 82 |
* when the count says rows should appear on the requested page yet none |
| 83 |
* were returned. An empty page past the end of a real result set |
| 84 |
* (offset >= count) is a legitimate empty, not a stale snapshot. |
| 85 |
* |
| 86 |
* @param int $liveCount negative or zero means "no rows / unavailable" |
| 87 |
* @param array<string, mixed> $tableOptions supplies perpage/paged |
| 88 |
* @return bool |
| 89 |
*/ |
| 90 |
public static function emptyRowsAreStale(int $liveCount, array $tableOptions): bool { |
| 91 |
if ($liveCount <= 0) { |
| 92 |
return false; |
| 93 |
} |
| 94 |
$perpage = isset($tableOptions['perpage']) && is_numeric($tableOptions['perpage']) |
| 95 |
? max(1, intval($tableOptions['perpage'])) : 25; |
| 96 |
$paged = isset($tableOptions['paged']) && is_numeric($tableOptions['paged']) |
| 97 |
? max(1, intval($tableOptions['paged'])) : 1; |
| 98 |
return $liveCount > ($paged - 1) * $perpage; |
| 99 |
} |
| 100 |
|
| 101 |
/** |
| 102 |
* Whether the last classified read is NOT a trustworthy "genuinely empty" |
| 103 |
* listing (pending, errored, or stale-empty). |
| 104 |
* |
| 105 |
* @return bool |
| 106 |
*/ |
| 107 |
public function wasIncomplete(): bool { |
| 108 |
return in_array($this->status, array( |
| 109 |
self::STATUS_PENDING, |
| 110 |
self::STATUS_ERRORED, |
| 111 |
self::STATUS_STALE_EMPTY, |
| 112 |
), true); |
| 113 |
} |
| 114 |
|
| 115 |
/** @return string One of the STATUS_* constants. */ |
| 116 |
public function status(): string { |
| 117 |
return $this->status; |
| 118 |
} |
| 119 |
} |
| 120 |
|