Перейти к основному содержимому

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
}
ПолеТипОписание
addedobject?Новые элементы / поля, появившиеся в матче. Иерархическая структура — пригодна для глубокого merge.
updatedobject?Изменившиеся существующие поля. Иерархическая структура.
flatChangesobject?Плоский формат с dot-notation путями (например homeScore.period1). Удобно для логирования и reactivity-store'ов.

Любое из трёх полей может отсутствовать, если соответствующих изменений нет.

Удаления полей в delta не приходят

Сервер отправляет только добавленные (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 выше).

Reconnect-стратегия

  1. При close (любой код кроме 1008) — переподключиться, см. Error codes → Recovery.
  2. Заново подписаться на нужные топики.
  3. Дождаться match_snapshot для каждого матча — игнорировать всё локальное состояние, перезалить из snapshot.
  4. Применять последующие match_delta поверх свежего snapshot.

Полный готовый пример: Recipes → Live matches feed.