# Загрузка данных и кеширование Этот документ описывает текущую реализацию загрузки данных и кеширования во frontend HiveTrace Guardrail Leaderboard. У реализации три основные цели: - хранить приватный Hugging Face token только на сервере SvelteKit; - не скачивать и не адаптировать повторно неизменившиеся payload-файлы bucket; - ускорить повторные переходы между страницами и сохранить предсказуемое окно актуальности данных. ## Архитектура ```text Browser -> SvelteKit page или __data.json request -> Node process в Hugging Face Space -> private Hugging Face bucket -> latest/manifest.json -> payload-файлы, указанные в manifest ``` Браузер не обращается к приватному bucket напрямую. Все запросы в bucket выполняет сервер SvelteKit с помощью server-only переменной `HF_TOKEN`. ## Источник данных Bucket настраивается runtime-переменными: ```text HF_TOKEN= HF_BUCKET_ID=hivetrace/leaderboard_frontend_v2 HF_BUCKET_PREFIX=latest HF_BUCKET_REQUEST_TIMEOUT_MS=60000 HF_BUCKET_REQUEST_RETRIES=2 HF_BUCKET_ENDPOINT=https://huggingface.co ``` `HF_BUCKET_ENDPOINT`, `HF_BUCKET_REQUEST_TIMEOUT_MS` и `HF_BUCKET_REQUEST_RETRIES` опциональны. Выше указаны их значения по умолчанию. `HF_BUCKET_CACHE_TTL_MS` по-прежнему управляет обычным in-memory кешем manifest и прямым helper Tools snapshot. Основной поток page data для Ranking, Details и Visualizations после промаха браузерного кеша явно обновляет manifest, поэтому эта переменная не добавляет еще одну задержку актуальности при обычных переходах по страницам. ## Manifest как указатель версии `latest/manifest.json` является единственным указателем версии для frontend. Основные поля: - `snapshot_id`: идентификатор опубликованного набора данных; - `files`: пути ко всем payload-файлам; - `hashes`: SHA-256 хеши payload-файлов; - `schema_version`: версия контракта bucket; - количества моделей, групп и датасетов для валидации. Frontend определяет изменение payload по `snapshot_id`. Каждая новая публикация обязана иметь новый уникальный `snapshot_id`. Изменение payload-файлов или хешей без изменения `snapshot_id` не поддерживается. В таком случае frontend считает snapshot прежним и может использовать старый in-memory payload до перезапуска процесса Space. ## Маршруты и payload-файлы | Маршрут | Server loader | Данные bucket | | -------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------- | | `/` | `getHfBucketRankingState()` | catalog, leaderboard и details matrix | | `/details` | `getHfBucketDetailsState()` | catalog, leaderboard и details matrix | | `/tools` | `getHfBucketToolsState()` | catalog, leaderboard, drilldown index, radar, scatter, heatmap, grouped bars, Pareto, performance и robustness | | `/methodology` | нет bucket page loader | payload из bucket не используется | Ranking также использует details matrix, потому что статус модели `partial` вычисляется по условию `metrics_evaluated_samples < sample_count`. Корневой `+layout.server.ts` отдельно читает manifest и возвращает публичный статус bucket: дату snapshot, количество моделей и другие общие поля. Этот статус использует обычный кеш manifest. Он является метаданными layout и не управляет актуальностью page data. ## Клиентская навигация SvelteKit перехватывает внутренние ссылки главного меню и запрашивает данные маршрутов через endpoint вида: ```text /__data.json /details/__data.json /tools/__data.json ``` Локализованные варианты также распознаются, например `/ru/details/__data.json`. Оптимизация response в `src/hooks.server.ts` применяется только к этим трем page-data маршрутам. Она не применяется к Methodology, статическим файлам, ошибкам и произвольным API response. ### Браузерный кеш Успешный page-data response получает заголовок: ```http Cache-Control: private, max-age=300 ``` Это означает: - браузер может повторно использовать response маршрута в течение пяти минут; - response не попадает в общий CDN или shared proxy cache; - окно `stale-while-revalidate` отсутствует; - после пяти минут следующий переход обязан обратиться к серверу SvelteKit до отрисовки маршрута. Кеш привязан к URL response, включая внутренние query-параметры SvelteKit. Поэтому Ranking, Details и Visualizations имеют отдельные записи браузерного кеша. Уже открытая страница сама не обновляется. Новые данные применяются при следующем переходе или полной перезагрузке страницы. ### Gzip-сжатие Тот же hook сжимает page-data response, когда выполнены все условия: - response успешный и содержит body; - заявленный размер не меньше 1 024 bytes; - клиент поддерживает `gzip`. Сжатый response содержит: ```http Content-Encoding: gzip Vary: Accept-Encoding ``` `Content-Length` удаляется, потому что сжатый body передается потоком. Клиенты без поддержки gzip получают исходный body с тем же приватным кешем на пять минут. Примерные измеренные размеры для текущего набора данных: | Данные маршрута | Без сжатия | Gzip | | --------------- | ---------: | -----: | | Ranking | 171 KB | 58 KB | | Details | 643 KB | 158 KB | | Visualizations | 709 KB | 122 KB | Размеры зависят от содержимого bucket и будут меняться при добавлении моделей и датасетов. ## Поток server request Если в браузере нет свежего page-data response, сервер выполняет следующие шаги: 1. Page loader вызывает соответствующую state-функцию Ranking, Details или Tools. 2. State-функция вызывает `fetchBucketRankingSnapshot({ refreshManifest: true })`. 3. `refreshManifest: true` обходит обычный TTL manifest и читает актуальный небольшой `manifest.json` из Hugging Face. 4. Если полученный `snapshot_id` совпадает с in-memory ranking snapshot, catalog и leaderboard используются повторно без скачивания payload. 5. Если `snapshot_id` изменился, catalog и leaderboard скачиваются параллельно, проверяются их хеши и schema, после чего ranking snapshot заменяется. 6. Данные конкретного маршрута переиспользуются или пересобираются для нового `snapshot_id`. 7. SvelteKit сериализует адаптированные данные маршрута, а server hook добавляет приватный кеш и gzip-сжатие. Разница между параметрами snapshot: - `refreshManifest: true` всегда проверяет manifest, но переиспользует payload при неизменном `snapshot_id`; - `forceRefresh: true` также отключает переиспользование payload и пересобирает ranking snapshot. Обычные переходы по страницам используют `refreshManifest`, а не `forceRefresh`. ## In-memory кеши сервера Все серверные кеши хранятся в памяти работающего Node process. Они принадлежат одному replica Space и очищаются при перезапуске, засыпании контейнера или новом deploy. ### Кеш manifest `src/lib/server/hf-bucket/cache.ts` хранит: - распарсенный manifest; - время его получения; - время истечения кеша; - один общий in-flight request manifest. Обычные callers используют `HF_BUCKET_CACHE_TTL_MS`, по умолчанию пять минут. Page-data refresh использует принудительное чтение manifest, описанное выше. Одновременные принудительные проверки подключаются к одному in-flight request, если пересекаются по времени. ### Ranking snapshot Ranking snapshot содержит manifest, catalog и leaderboard. Он кешируется по `snapshot_id`. Одновременные загрузки одного snapshot используют общий promise. In-flight request также привязан к `snapshot_id`, поэтому запрос нового опубликованного snapshot не использует по ошибке загрузку предыдущего snapshot. ### Details snapshot Details snapshot добавляет `details_matrix` к ranking snapshot и кешируется по `snapshot_id`. Этот кеш общий для Ranking и Details. Ranking использует matrix для определения `partial`, поэтому после открытия Ranking страница Details не скачивает и не валидирует `details_matrix` повторно. Одновременные запросы одного details snapshot также используют общий promise. ### Адаптированные Ranking и Details Готовые для UI объекты Ranking и Details кешируются по `snapshot_id`. Если manifest не изменился, сервер возвращает уже адаптированный объект. Новый snapshot парсится, валидируется и адаптируется один раз на Node process. ### Visualizations snapshot и адаптированный Tools dataset Для нового snapshot файлы визуализаций скачиваются параллельно. Raw visualization snapshot и адаптированный Tools dataset кешируются отдельно по `snapshot_id`. Прямой helper `getHfBucketToolsSnapshot()` сохраняет свой быстрый путь на основе TTL. Страница `/tools` использует state-путь, который обновляет manifest после промаха браузерного кеша. ## Валидация Payload используется повторно только после проверки версии manifest. Новый payload проходит: - JSON parsing; - schema parsing; - проверку SHA-256 при наличии хеша; - сверку количеств из manifest; - проверку ссылок на модели, группы и датасеты; - проверку полноты matrix и дублирующихся пар; - проверку ссылок в данных визуализаций. Ошибка валидации обрабатывается так же, как другая ошибка обновления. ## Гарантия актуальности данных Предположим, что корректный новый snapshot опубликован в момент `T`, а bucket доступен. ### Response маршрута уже находится в браузерном кеше Старый response может использоваться до истечения его индивидуального `max-age` в пять минут. Первый переход на этот маршрут после истечения кеша читает свежий manifest. Если `snapshot_id` изменился, тот же переход ожидает загрузку нового payload и получает новые данные. Ожидаемая гарантия: > Не позднее первого перехода после истечения пятиминутного браузерного кеша маршрута > пользователь получает новый snapshot. Дополнительного окна выдачи stale response нет. ### Response маршрута отсутствует в браузерном кеше Переход сразу обращается к серверу и проверяет manifest. Если новый payload уже находится в памяти сервера, он используется повторно. Иначе переход ожидает скачивание и адаптацию нового snapshot. ### Полная перезагрузка страницы Пятиминутная политика применяется к SvelteKit navigation response `__data.json`. Обычный полный HTML request не покрывается этим page-data кешем и снова запускает server page loader. ### Исключения Пятиминутное ожидание неприменимо, если: - Hugging Face или сеть недоступны; - новый payload не проходит parsing, проверку хеша или валидацию; - publisher повторно использовал старый `snapshot_id`; - manifest опубликован раньше, чем стали доступны все указанные в нем файлы. ## Поведение при ошибках Кеш также обеспечивает устойчивость: - если refresh завершился ошибкой и готовые данные маршрута существуют, сервер возвращает кеш; - если новый manifest доступен, но новый payload невалиден или недоступен, route state становится `stale`, и UI может показать предупреждение; - если пригодного кеша нет, маршрут возвращает состояние `unavailable` с пустым fallback dataset; - одновременные запросы используют общую in-flight работу и не дублируют загрузку bucket. Важная деталь текущей реализации: если принудительное чтение manifest завершилось ошибкой, но в памяти есть предыдущий manifest, слой manifest может вернуть его как stale. После этого сервер продолжит выдавать предыдущие route data. Текущий route state не во всех случаях пробрасывает именно этот fallback manifest как видимое предупреждение `stale`. ## Публикация нового snapshot Producer должен выполнять публикацию атомарно с точки зрения frontend: 1. Сгенерировать все payload-файлы. 2. По возможности использовать immutable или snapshot-specific пути. 3. Рассчитать и записать SHA-256 хеши. 4. Загрузить каждый payload-файл и проверить, что он доступен для чтения. 5. Создать manifest с новым уникальным `snapshot_id`, корректными путями, хешами, количествами и версией schema. 6. Последним загрузить `latest/manifest.json`. Публикация manifest последним не позволяет frontend увидеть новую версию, которая ссылается на еще недоступные файлы. ## Cold start и replicas После перезапуска или cold start Space in-memory snapshot отсутствует. Первый request должен прочитать manifest и все payload-файлы, необходимые выбранному маршруту. Следующие запросы используют подготовленные данные повторно. Если Space работает с несколькими replicas, каждая имеет собственный кеш в памяти и прогревается независимо. Браузерный кеш остается локальным для каждого пользователя. ## Операционная проверка Для проверки заголовков page-data response нужен авторизованный запрос к Space: ```bash curl -sS \ -H "Authorization: Bearer $HF_TOKEN" \ -H "Accept-Encoding: gzip" \ -D - \ -o /dev/null \ "https:///details/__data.json" ``` Ожидаемые заголовки: ```http Cache-Control: private, max-age=300 Content-Encoding: gzip Vary: Accept-Encoding ``` `curl` не воспроизводит navigation cache браузера без отдельной настройки собственного кеша. Поэтому повторные curl-запросы доходят до сервера и могут повторно запускать проверку manifest. При проверке новой публикации нужно убедиться, что: - manifest содержит новый `snapshot_id`; - каждый указанный файл существует; - хеши соответствуют опубликованному содержимому; - в логах Space нет ошибок валидации bucket; - первый переход после истечения браузерного кеша показывает новый snapshot. ## Карта реализации | Ответственность | Файл | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------ | | HTTP client bucket и server-only token | `src/lib/server/hf-bucket/client.ts` | | Обычный кеш manifest и in-flight request | `src/lib/server/hf-bucket/cache.ts` | | Загрузка и валидация Ranking snapshot | `src/lib/server/hf-bucket/ranking-snapshot.ts` | | Общий Details snapshot | `src/lib/server/hf-bucket/details-snapshot.ts` | | Загрузка Visualization snapshot | `src/lib/server/hf-bucket/tools-snapshot.ts` | | Кеш адаптированного Ranking | `src/lib/server/hf-bucket/ranking-cache.ts` | | Кеш адаптированного Details | `src/lib/server/hf-bucket/details-cache.ts` | | Raw и адаптированный кеш Tools | `src/lib/server/hf-bucket/tools-cache.ts` | | Заголовки браузерного кеша и gzip | `src/hooks.server.ts` | | Публичный статус bucket в root layout | `src/routes/+layout.server.ts` | | Оркестрация маршрутов | `src/routes/+page.server.ts`, `src/routes/details/+page.server.ts`, `src/routes/tools/+page.server.ts` |