$tableOptions * @return string */ private function getViewSnapshotCacheKey($prefix, $sub, $tableOptions) { $cacheShape = array( 'sub' => (string)$sub, 'filter' => is_scalar($tableOptions['filter'] ?? 0) ? (int)($tableOptions['filter'] ?? 0) : 0, 'orderby' => is_scalar($tableOptions['orderby'] ?? 'url') ? (string)($tableOptions['orderby'] ?? 'url') : 'url', 'order' => is_scalar($tableOptions['order'] ?? 'ASC') ? (string)($tableOptions['order'] ?? 'ASC') : 'ASC', 'paged' => is_scalar($tableOptions['paged'] ?? 1) ? (int)($tableOptions['paged'] ?? 1) : 1, 'perpage' => is_scalar($tableOptions['perpage'] ?? ABJ404_OPTION_DEFAULT_PERPAGE) ? (int)($tableOptions['perpage'] ?? ABJ404_OPTION_DEFAULT_PERPAGE) : ABJ404_OPTION_DEFAULT_PERPAGE, 'filterText' => is_scalar($tableOptions['filterText'] ?? '') ? (string)($tableOptions['filterText'] ?? '') : '', 'score_range' => (function ($v) { return is_string($v) ? $v : 'all'; })($tableOptions['score_range'] ?? 'all'), 'blog' => function_exists('get_current_blog_id') ? (int)get_current_blog_id() : 1, ); $encoded = function_exists('wp_json_encode') ? wp_json_encode($cacheShape) : json_encode($cacheShape); return $prefix . '_' . md5((string)$encoded); } /** @return void */ private function ensureViewSnapshotTableExists(): void { if (self::$viewSnapshotTableEnsured) { return; } self::$viewSnapshotTableEnsured = true; $sqlFile = __DIR__ . '/sql/createViewCacheTable.sql'; $create = ABJ_404_Solution_Functions::readFileContents($sqlFile); if (is_string($create) && trim($create) !== '') { $this->queryAndGetResults($create, array('log_errors' => false)); } } /** @param string $cacheKey @return string */ private function getViewSnapshotLockOptionName(string $cacheKey): string { return $this->getLowercasePrefix() . 'abj404_view_cache_lock_' . md5((string)$cacheKey); } /** @param string $cacheKey @return bool */ private function isViewSnapshotRefreshLocked(string $cacheKey): bool { if (!function_exists('get_option')) { return false; } $lockKey = $this->getViewSnapshotLockOptionName($cacheKey); $lockValue = get_option($lockKey, false); if ($lockValue === false || $lockValue === '' || $lockValue === null) { return false; } $lockTs = is_numeric($lockValue) ? (int)$lockValue : 0; if ($lockTs > 0 && (time() - $lockTs) > self::VIEW_SNAPSHOT_REFRESH_COOLDOWN_SECONDS) { if (function_exists('delete_option')) { delete_option($lockKey); } return false; } return true; } /** @param string $cacheKey @return bool */ private function acquireViewSnapshotRefreshLock(string $cacheKey): bool { if (!function_exists('add_option')) { return true; } if ($this->isViewSnapshotRefreshLocked($cacheKey)) { return false; } $lockKey = $this->getViewSnapshotLockOptionName($cacheKey); return (bool)add_option($lockKey, time(), '', false); } /** @param string $cacheKey @return void */ private function releaseViewSnapshotRefreshLock(string $cacheKey): void { if (function_exists('delete_option')) { delete_option($this->getViewSnapshotLockOptionName($cacheKey)); } } /** * @param mixed $payload * @return array|null */ private function decodeSnapshotPayload($payload) { if (!is_string($payload) || $payload === '') { return null; } $decoded = json_decode($payload, true); return is_array($decoded) ? $decoded : null; } /** * @param string $cacheKey * @param bool $allowExpired * @param bool $respectCooldown * @return array|null */ private function getViewRowsSnapshotFromTable(string $cacheKey, bool $allowExpired = false, bool $respectCooldown = false) { $this->ensureViewSnapshotTableExists(); $query = "SELECT payload, refreshed_at, expires_at FROM {wp_abj404_view_cache} WHERE cache_key = %s LIMIT 1"; $result = $this->queryAndGetResults($query, array('query_params' => array($cacheKey), 'log_errors' => false)); if (!is_array($result['rows']) || empty($result['rows']) || !is_array($result['rows'][0])) { return null; } $row = $result['rows'][0]; $expiresAt = intval($row['expires_at'] ?? 0); $refreshedAt = intval($row['refreshed_at'] ?? 0); $now = time(); $isFresh = ($expiresAt > $now); $recentEnough = ($refreshedAt > 0 && ($now - $refreshedAt) <= self::VIEW_SNAPSHOT_REFRESH_COOLDOWN_SECONDS); if (!$allowExpired && !$isFresh) { return null; } if ($respectCooldown && !$isFresh && !$recentEnough) { return null; } return $this->decodeSnapshotPayload((string)($row['payload'] ?? '')); } /** * @param string $cacheKey * @param string $sub * @param mixed $rows * @param int $ttlSeconds * @return void */ private function setViewRowsSnapshotToTable(string $cacheKey, string $sub, $rows, int $ttlSeconds): void { if (!is_array($rows)) { return; } $this->ensureViewSnapshotTableExists(); $encoded = function_exists('wp_json_encode') ? wp_json_encode($rows) : json_encode($rows); if (!is_string($encoded)) { return; } $bytes = strlen($encoded); if ($bytes > self::VIEW_SNAPSHOT_MAX_PAYLOAD_BYTES) { return; } $now = time(); $expiresAt = $now + max(1, intval($ttlSeconds)); $query = "INSERT INTO {wp_abj404_view_cache} (cache_key, subpage, payload, payload_bytes, refreshed_at, expires_at, updated_at) VALUES (%s, %s, %s, %d, %d, %d, %d) ON DUPLICATE KEY UPDATE subpage = VALUES(subpage), payload = VALUES(payload), payload_bytes = VALUES(payload_bytes), refreshed_at = VALUES(refreshed_at), expires_at = VALUES(expires_at), updated_at = VALUES(updated_at)"; $this->queryAndGetResults($query, array( 'query_params' => array($cacheKey, (string)$sub, $encoded, $bytes, $now, $expiresAt, $now), 'log_errors' => false, )); $this->cleanupExpiredViewSnapshotRowsIfNeeded(); } /** * @param string $cacheKey * @param int $timeoutMs * @return array|null */ private function waitForViewRowsSnapshotFromTable(string $cacheKey, int $timeoutMs = 4000) { $deadline = microtime(true) + (max(100, intval($timeoutMs)) / 1000); while (microtime(true) < $deadline) { $rows = $this->getViewRowsSnapshotFromTable($cacheKey, false, false); if (is_array($rows)) { return $rows; } usleep(100000); } return null; } /** @return void */ private function cleanupExpiredViewSnapshotRowsIfNeeded(): void { if (!function_exists('get_transient') || !function_exists('set_transient')) { return; } $marker = get_transient('abj404_view_cache_cleanup_marker'); if ($marker !== false) { return; } set_transient('abj404_view_cache_cleanup_marker', time(), 1800); $query = "DELETE FROM {wp_abj404_view_cache} WHERE expires_at < %d"; $this->queryAndGetResults($query, array( 'query_params' => array(time() - self::VIEW_SNAPSHOT_REFRESH_COOLDOWN_SECONDS), 'log_errors' => false, )); } }