Snapshot vs Delta
После подписки сервер шлёт обновления матча двумя видами сообщений: match_snapshot (полное состояние) и match_delta (только изменения относительно предыдущего snapshot).
match_snapshot
Полное состояние матча — поле data идентично ответу REST GET /v2/{sportSlug}/matches/{matchId}.
{
"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).
Какие поля меняются
- Часто (live-update):
homeScore,awayScore,currentMatchMinute,currentMatchSecond,status,matchStatistics,liveEvents,oddsBase. - Однократно по ходу матча:
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.