🕸️ dicechess-sync — служебная справка
dicechess-sync — приватный сервис бэкфилла: он обходит граф игроков dicechess.com, скачивает исторические партии, нормализует их и заливает в аналитику, попутно зеркаля сырьё в bronze-архив.
Эта страница — операционная справка «как запускать и настраивать». Как оно устроено по существу — в спайне:
- архитектура и место в системе — 03-Sinkhronizatsiya-obzor-i-arkhitektura;
- граф игроков и перечисление — 04-Graf-igrokov-i-perechislenie;
- конвейер партии и машины состояний — 05-Konveyer-partiy-i-sostoyaniya;
- rate-limiting и обход Cloudflare — 06-Zashchita-ot-blokirovok-i-Cloudflare;
- контракт ingest и валидация движком — 07-Kontrakt-ingest-i-validatsiya-dvizhkom;
- идентичность и дедупликация — 08-Identichnost-istochniki-i-deduplikatsiya.
Почему репозиторий приватный
Он содержит обратную разработку (reverse engineering) приватного API dicechess.com — публичного разрешения на автоматический сбор нет, поэтому вся специфика скрейпинга и обхода защиты вынесена в приватный контур. Публичные проекты (
dicechess-analytics,dicechess-engine-scala) остаются чистыми от специфики сайта — их граница — нейтральный контрактPOST /api/games.
1. Стек и устройство
- Node ≥ 26, TypeScript исполняется нативно (type-stripping, без шага сборки).
type: module. - Состояние — локальная SQLite (
./data/sync.db, в Docker/app/data/sync.db), WAL, мигратор поPRAGMA user_version(подключение к ней описано в инструкции по подключению). Таблицы:players(фронтир, см. 04-Graf-igrokov-i-perechislenie),games+raw_game_data(каталог + сырой кэш, см. 05-Konveyer-partiy-i-sostoyaniya),app_meta. - Транспорт к сайту —
curl-subprocess (Cloudflare фингерпринтит TLS), к аналитике — обычныйfetch. ОдинRateLimiterна процесс. См. 06-Zashchita-ot-blokirovok-i-Cloudflare. - Образ —
node:26-trixie-slim(OpenSSL 3.5), приватный пакет@rabestro/dicechess-raw-archiveтянется через BuildKit-secret.
2. CLI-команды
Все запускаются нативным Node без сборки. Требуют DICECHESS_JWT в окружении.
| Команда | Назначение |
|---|---|
node src/sync-player.ts <userId> [maxFetch] | Бэкфилл одного игрока end-to-end: enumerate → fetch (≤ maxFetch, по умолчанию ∞) → post. Резюмируемо: повторный запуск синхронизированного игрока делает инкрементальный свип от курсора synced_through_ms. |
node src/sync-crawl.ts <maxPlayers> <maxFetch> [seedId,…] | Одноразовый ограниченный обход графа: seed-игроки добавляются как manual, дальше спайдер по приоритету. RESYNC_BEFORE=<ISO> переочередит устаревших синхронизированных. |
node src/crawl-service.ts | Долгоиграющий демон (Docker CMD) — повторяет ограниченные проходы, пока фронтир не опустеет, затем простаивает (как смотреть вывод в реальном времени описано в руководстве по логам). См. §4. |
node src/sync-archive.ts | Одноразово дотолкнуть всё кэшированное сырьё в bronze-архив на dexus. Требует ARCHIVE_DB_URL. |
Сидирование графа
Сейчас фронтир сидируется вручную —
SEED_IDS(или аргументомsync-crawl), игроки попадают сdiscovered_via='manual', дальше граф расходится спайдером по оппонентам. Лидерборды (leaderboard/x2_leaderboard) — задуманный источник seed’ов и зарезервированы как значенияdiscovered_via, но автоматический фетч лидербордов пока не реализован. Подробнее про обход — 04-Graf-igrokov-i-perechislenie.
3. Переменные окружения
| Переменная | По умолчанию | Назначение |
|---|---|---|
DICECHESS_JWT | — (обязательно) | Bearer-JWT сайта (из браузера, живёт ~месяцы). |
ANALYTICS_BASE_URL | http://192.168.10.3:8020 | База ingest-аналитики (aurora). |
ANALYTICS_INGEST_TOKEN | '' | Bearer для POST /api/games. |
DB_PATH | ./data/sync.db | Путь к SQLite. |
ARCHIVE_DB_URL | '' (пусто → архив пропускается) | DSN bronze-архива (dexus :5433). |
DRY_RUN | false | 1/true → enumerate+fetch+cache, без POST. |
REQUEST_MIN_DELAY_MS | 3000 | Базовый интервал между запросами к сайту. (1500 был слишком быстр — ловил 429.) |
REQUEST_JITTER_MS | 1500 | Случайная добавка [0, jitter). |
REQUEST_MAX_RETRIES | 5 | Ретраи запроса при throttle (ждёт cooldown, повторяет тот же вызов). |
MAX_REQUESTS_PER_HOUR | ∞ (не задано) | Часовой бюджет (окно 1 ч). Не ловит минутный всплеск — это делает spacing. |
ENUMERATE_PAGE_SIZE | 500 | Размер страницы player/history. Главный рычаг против лимита перечисления (для китов поднимают до 1000). |
SEED_IDS | [] | id игроков для посева фронтира (демон), через запятую. |
BATCH_PLAYERS | 25 | Игроков на проход демона. |
BATCH_FETCH | 200 | Партий-фетчей на проход демона. |
INTER_PASS_MS | 0 | Пауза между проходами, пока есть работа. |
IDLE_MS | 300000 (5 мин) | Сон, когда фронтир опустел. |
RESYNC_AFTER_MS | 0 | >0: переочередить игроков, синхронизированных давнее этого (инкрем. рефреш демона). |
Уроки и тонкости лимитов (429, асимметрия player/history vs game-move-history, pageSize) — 06-Zashchita-ot-blokirovok-i-Cloudflare (детальное описание интервалов и пауз демона см. в документации по интервалам).
4. Демон crawl-service
Один процесс, один RateLimiter (глобальный single-flight). На старте: resetInProgress (возврат подвисших in_progress → pending), посев SEED_IDS. Цикл прохода while (!stop):
flowchart TD A["crawlFrontier<br/>(≤ BATCH_PLAYERS enumerate,<br/>≤ BATCH_FETCH fetch, затем drain post)"] --> B["лог: players/games/fetch/post/<br/>frontier/cooldown"] B --> C["archive push (если ARCHIVE_DB_URL)<br/>НЕ фатально — не роняет цикл"] C --> D{"был прогресс?"} D -->|"да"| E["nap INTER_PASS_MS → проход заново"] D -->|"нет"| F{"RESYNC_AFTER_MS > 0?"} F -->|"да, есть устаревшие"| G["requeueStale → проход заново"] F -->|"нет"| H["idle: nap IDLE_MS"] E --> A G --> A H --> A
SIGTERM/SIGINT прерывают сон и останавливают цикл аккуратно; в finally закрываются архив и БД. Архив-пуш каждого прохода идемпотентен и обёрнут в try/catch — недоступность dexus никогда не валит краулинг (сырьё остаётся в локальном кэше). Полная стадия архивации — 05-Konveyer-partiy-i-sostoyaniya.
5. Деплой
- Образ
ghcr.io/rabestro/dicechess-sync:latest(+:vX.Y.Z), мульти-арч (в т.ч. arm64 под Raspberry Pi). docker-compose:restart: unless-stopped, томsync_data:/app/data(долговечное состояние),env_file: .env(описание выбора портов для веб-админки базы на RPi см. в отчете по портам).- Крутится на rpi4 (residential IP — обязателен для прохождения Cloudflare; с дата-центрового/ноутбучного IP
curlловит403).
6. Резюмируемость и идемпотентность (кратко)
Весь прогресс — в SQLite, любой энтрипоинт и демон рестартятся свободно: resetInProgress чинит подвисшее, enumerate_offset резюмирует перечисление кита, synced_through_ms превращает повторный запуск в дешёвый инкрементальный свип. Идемпотентность: каталог INSERT OR IGNORE (партия из историй обоих игроков заводится один раз), ingest 201/200, архив first-writer-wins + archived_at. Реплей из сырья (replayRejected / replayNormalizeFailures) переконвертирует без обращения к сайту. Детали — 05-Konveyer-partiy-i-sostoyaniya.
Связанное: 03-Sinkhronizatsiya-obzor-i-arkhitektura, Raw-arkhiv-bronze-sloy, Apgreyd-aurora-plan-i-chek-list, SSH-tunnel-k-servernomu-Postgres.