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

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
}
ПолеТипОписание
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). Единственное исключение из 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 выше).

Reconnect-стратегия

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

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