8388608) return null; $entry = json_decode(file_get_contents($cache), true); return is_array($entry) && ($entry['key'] ?? '') === $key && ($entry['expires'] ?? 0) > time() && is_array($entry['data'] ?? null) ? $entry['data'] : null; }; $data = $load(); if ($data !== null) return $data; $lock = localnh_job_lock('aggregate-' . $kind); if (!$lock) localnh_job_error(503, 'collection summary is being refreshed. please retry.'); $data = $load(); if ($data !== null) return $data; require_once __DIR__ . '/collection_reads.php'; $imagesLock = localnh_collection_read_guard(); $tmp = "$root/aggregate-$kind.part"; if (is_link($tmp)) throw new RuntimeException('Linked aggregate workspace'); if (!localnh_job_run([__DIR__ . '/venv/bin/python', __DIR__ . '/script/py/aggregate.py', $kind], $tmp, true, null, [$lock, $imagesLock])) throw new RuntimeException('Aggregate builder failed or exceeded limits'); $data = json_decode(file_get_contents($tmp), true, 32, JSON_THROW_ON_ERROR); if (!is_array($data) || localnh_aggregation_key() !== $key) throw new RuntimeException('Collection changed during aggregation'); $entry = json_encode(['key' => $key, 'expires' => time() + 15, 'data' => $data], JSON_THROW_ON_ERROR); if (strlen($entry) > 8388608) throw new RuntimeException('Aggregate exceeds cache limit'); if (file_put_contents($tmp, $entry) === false || !rename($tmp, $cache)) throw new RuntimeException('Aggregate publication failed'); return $data; } catch (Throwable $e) { error_log('Collection aggregation failed: ' . $e->getMessage()); } finally { if ($tmp !== null && is_file($tmp)) unlink($tmp); if (is_resource($lock)) fclose($lock); if (isset($imagesLock) && is_resource($imagesLock)) fclose($imagesLock); } localnh_job_error(503, 'collection summary unavailable. please retry.'); }