Snapshot vs Delta
После подписки сервер шлёт обновления матча двумя видами сообщений: match_snapshot (полное состояние) и match_delta (только изменения относительно предыдущего snapshot).
match_snapshot
Полное состояние матча — поле data идентично ответу REST GET /v2/{sportSlug}/matches/{matchId}. Если подписка оформлена с withBkOdds: true, snapshot дополнительно включает блок oddsBk с коэффициентами всех подключённых букмекеров — как REST с with_bk_odds=true (у матча без коэффициентов — пустой объект {}; при кратковременной деградации источника кэфов поле может отсутствовать — трактуйте как «нет данных»). Без флага oddsBk в снапшоте не приходит.
{
"type": "match_snapshot",
"matchId": 12345678,
"sportSlug": "football",
"data": {
"id": 12345678,
"status": "inprogress",
"currentMatchMinute": 67,
"homeScore": { "current": 2, "period1": 1, "period2": 1 },
"awayScore": { "current": 1, "period1": 1, "period2": 0 }
/* …полная структура Match */
},
"timestamp": 1714123200000
}
match_snapshot (полный кадр) отправляется в следующих случаях:
- Сразу после
subscribeна конкретный матч (action: "subscribe", type: "match") — сервер тут же шлёт snapshot с актуальным состоянием. - Когда сервер не может вычислить delta — если предыдущее состояние матча недоступно, вместо delta уходит полный матч.
- После reconnect — переподписавшись на матч (см. первый пункт), клиент снова получает свежий snapshot и может выбросить старое локальное состояние.
match_delta
Инкрементальные изменения относительно последнего snapshot для этого матча. Поле changes содержит три параллельных представления одного и того же дифа.
{
"type": "match_delta",
"matchId": 15370088,
"sportSlug": "tennis",
"changes": {
"added": {
"matchStatistics": {
"0": {
"groups": {
"1": {
"groupName": "Points",
"statisticsItems": [ /* ... */ ]
}
}
}
}
},
"updated": {
"homeScore": { "period1": 0 },
"awayScore": { "period1": 0 }
},
"flatChanges": {
"matchStatistics.0.groups.1.groupName": "Points",
"homeScore.period1": 0,
"awayScore.period1": 0
}
},
"timestamp": 1768461968409
}
| Поле | Тип | Описание |
|---|---|---|
added | object? | Новые элементы / поля, появившиеся в матче. Иерархическая структура — пригодна для глубокого merge. |
updated | object? | Изменившиеся существующие поля. Иерархическая структура. |
flatChanges | object? | Плоский формат с dot-notation путями (например homeScore.period1). Удобно для логирования и reactivity-store'ов. |
Любое из трёх полей может отсутствовать, если соответствующих изменений нет.
Сервер отправляет только добавленные (added) и изменённые (updated) поля. Если поле
исчезло из матча, в delta это не отражается — клиент сохранит прежнее значение до следующего
match_snapshot. На практике поля матча по ходу события почти не удаляются.
Применение delta
Стандартный подход — deep-merge added и updated поверх предыдущего snapshot. Поле flatChanges обычно избыточно при наличии deep-merge и нужно только если ваш стор работает по «плоским» путям.
const state = new Map(); // matchId → полная структура Match
function applyMessage(msg) {
if (msg.type === 'match_snapshot') {
state.set(msg.matchId, msg.data);
return;
}
if (msg.type === 'match_delta') {
const prev = state.get(msg.matchId);
if (!prev) return; // delta пришла раньше snapshot — игнорируем
let next = prev;
if (msg.changes.added) next = deepMerge(next, msg.changes.added);
if (msg.changes.updated) next = deepMerge(next, msg.changes.updated);
state.set(msg.matchId, next);
}
}
deepMerge — любая стандартная реализация (например, lodash.merge, npm-пакет deepmerge). Единственное исключение из deep-merge — блоки oddsBk.<bookmaker>, они заменяются целиком (см. следующий раздел).
Дельты коэффициентов букмекеров (oddsBk)
Дельты кэфов приходят только по подпискам с
withBkOdds: true. Подписка без флага работает как раньше: ниoddsBkв снапшоте, ни дельт коэффициентов.
Коэффициенты букмекеров обновляются отдельным потоком: сервер отслеживает изменения кэфов каждого подключённого букмекера и шлёт opt-in подписчикам матча match_delta, где changes.updated.oddsBk.<bookmaker> — блок этого букмекера целиком (та же структура BookmakerOddsData, что и в REST/match_snapshot):
{
"type": "match_delta",
"matchId": 12345678,
"sportSlug": "football",
"changes": {
"added": {},
"updated": {
"oddsBk": {
"melbet": {
"slug": "melbet",
"name": { "en": "Melbet", "ru": "Мелбет" },
"updatedAt": 1714123200000,
"isBettingActive": true,
"markets": { /* полный набор рынков букмекера */ }
}
}
},
"flatChanges": {
"oddsBk.melbet": { /* тот же блок целиком; ключ = слаг букмекера: melbet | pari | marathon | betboom */ }
}
},
"timestamp": 1714123200000
}
Особенности:
- Дельта приходит только при реальном изменении значений — сервер сравнивает сами кэфы/рынки, а не таймстемпы обновления фида. Нет изменений — нет сообщений.
- Блок заменяется целиком, НЕ deep-merge. Рынок или исход может исчезнуть из блока (линия закрыта букмекером) — deep-merge оставил бы в вашем состоянии устаревший рынок. Правильное применение:
if (msg.type === 'match_delta' && msg.changes.updated && msg.changes.updated.oddsBk) {
const prev = state.get(msg.matchId);
if (prev) {
prev.oddsBk = { ...(prev.oddsBk || {}) };
for (const [bk, block] of Object.entries(msg.changes.updated.oddsBk)) {
prev.oddsBk[bk] = block; // замена блока букмекера целиком
}
}
}
- В одном сообщении приходит блок одного букмекера (изменения у разных букмекеров независимы);
addedв таких дельтах пустой. - Приходят подписчикам обоих типов топиков — и
match:{slug}:{id}, иsport:{slug}— но только тем, кто подписался сwithBkOdds: true. - Появление коэффициентов у матча (флаги
hasBkOdds) по-прежнему приходит обычной дельтой полей матча — всем подписчикам, флаг не требуется.
match_deleted
Если матч удалён из ленты данных (встреча убрана или пересоздана в расписании), подписчики матча и спорт-канала получают уведомление:
{
"type": "match_deleted",
"matchId": 12345678,
"sportSlug": "football",
"timestamp": 1783420000000
}
Получив match_deleted, прекратите ожидать обновления по этому матчу — match_snapshot/match_delta по нему больше не придут. Подписка на топик матча при этом не разрывается автоматически: отпишитесь (unsubscribe) или закройте её вместе с соединением. Если встреча была пересоздана в расписании, она появится под новым matchId — найти её можно через REST (GET /v2/{sportSlug}/matches?date=... или поиск ?q=).
Какие поля меняются
- Часто (live-update):
homeScore,awayScore,currentMatchMinute,currentMatchSecond,status,matchStatistics,liveEvents,oddsBase,oddsBk(блок букмекера целиком — см. выше). - Однократно по ходу матча:
homeTeam.lineup/awayTeam.lineup(за час-два до старта),referee(часто за день),venue(статика на матч). - Не меняется:
id,tournament,season,category,startTimestamp,homeTeam.id,awayTeam.id,dateEvent.
Sport-specific delta-поля
В changes.added / changes.updated встречаются как универсальные поля (homeScore, awayScore, currentMatchMinute, status, matchStatistics, oddsBase), так и sport-specific:
- Tennis (
tennis.*):tennis.sets[N](по каждому сыгранному сету),tennis.momentum[N](новые точки на momentum-графике),tennis.pointByPoint[N](новые розыгрыши — в WS-потоке приходят независимо от типа подписки). - Esports (
esports.*):esports.games[N](по каждой карте серии),esports.games[N].statistics(обновления командных счётчиков),esports.games[N].rounds.normaltime[N](новый CS2-раунд),esports.games[N].bans(драфт MOBA),esports.games[N].homeTeamPlayers[N]/awayTeamPlayers[N](обновления KDA/ADR). - Football:
referee.*(если данные подгружаются позже старта),liveEvents[N](новые события матча).
Полный список sport-specific полей по виду спорта — см. Sport-specific data.
Подписка на спорт vs на матч
Оба типа подписки получают обновления через один и тот же механизм Change Stream, а сообщения
форматируются с детализацией (как REST detail-эндпоинт), поэтому liveEvents и
tennis.pointByPoint присутствуют в потоке независимо от типа подписки — в отличие от REST-списка,
где этих полей нет. Разница между подписками — в начальном snapshot и охвате.
subscribe на конкретный матч (match:<slug>:<id>)
- Сразу приходит
match_snapshotс полным состоянием. - Дальше — обновления только по этому матчу.
subscribe на весь спорт (sport:<slug>)
- Обновления приходят по всем матчам спорта, у которых произошли изменения. Держите
MapпоmatchId, чтобы разводить состояния. - ⚠️ Начальный snapshot по каждому матчу НЕ высылается. Первое сообщение по матчу может оказаться
match_delta, который нельзя применить без предыдущего состояния. Поэтому для спорт-подписки:- засейте текущее состояние через REST (например, список матчей
GET /v2/{sportSlug}/matches), затем применяйте delta поверх; - любой пришедший
match_snapshotтрактуйте как полный сброс состояния этого матча; match_deltaдля матча, которого ещё нет в вашемMap, игнорируйте до прихода snapshot (как в примереapplyMessageвыше).
- засейте текущее состояние через REST (например, список матчей
Reconnect-стратегия
- При close (любой код кроме
1008) — переподключиться, см. Error codes → Recovery. - Заново подписаться на нужные топики.
- Дождаться
match_snapshotдля каждого матча — игнорировать всё локальное состояние, перезалить из snapshot. - Применять последующие
match_deltaповерх свежего snapshot.
Полный готовый пример: Recipes → Live matches feed.