Состояние спортивного события: счёт, фазы и завершение матча
Счёт, таймер и статус матча нельзя обновлять как независимые поля: так легко получить невозможное состояние. Разбираем state machine, нормализованные команды и правила приоритета для live-события.

Состояние события — это не набор независимых полей
Если отдельно обновлять clock, score, phase и is_finished, легко получить невозможную комбинацию: «матч завершён», но таймер идёт; новая атака приходит после final; halftime внезапно откатывается в first half. Нужна модель допустимых переходов и приоритетов.
Сырой фид не управляет интерфейс напрямую
Provider adapter сначала приводит события к нормализованным командам: SCORE_CHANGED, PHASE_CHANGED, CLOCK_SYNC, EVENT_FINISHED. State machine применяет команду только если переход допустим для текущего состояния и версии. UI читает уже собранный event snapshot.
Строгость против «показывать хоть что-то»
Слишком строгая машина может отбросить полезное обновление от нестабильного поставщика. Слишком мягкая превращает ошибку поставщика в ошибку продукта. Практичный вариант — различать hard transition и soft projection: счёт можно обновить при восстановлении, а фазу турнира нельзя откатить без специального reconciliation-события.
Приоритет событий
EVENT_FINISHED закрывает обычные live-переходы, но допускает correction-класс событий. CLOCK_SYNC не продвигает фазу. PHASE_CHANGED может сбросить локальную интерполяцию таймера. Все команды имеют source sequence или внутреннюю version, чтобы запоздавшее событие не перезаписало новое состояние.
Невозможный переход — отдельный сигнал
Нельзя просто логировать reject строкой. Нужен счетчик по типу перехода и поставщику, плюс сохранение компактного контекста: current state, command, provider sequence. Это позволяет отличить баг mapping от реального out-of-order в feed.
Какие инварианты стоит записать явно
State machine полезна не сама по себе, а потому что заставляет сформулировать невозможные сочетания. Завершённый матч не должен снова стать live из-за позднего пакета; таймер не должен идти в паузе; счёт из более старой версии не должен перезаписывать новый. Эти правила лучше хранить рядом с переходами, а не распределять по условным операторам в UI.
Когда инвариант нарушен, система должна оставить след: входное событие, предыдущий state, версия и решение reconciliation. Тогда спорный случай можно восстановить по журналу, а не пытаться угадать, почему на экране несколько секунд был невозможный статус.
- Определить терминальные состояния и запретить переходы из них без специального reconciliation.
- Присваивать событиям порядок или версию, если поставщик это позволяет.
- Отделить отображаемый таймер от доменного статуса матча.
Невозможное состояние должно шуметь
Допустим, событие уже помечено final, но через несколько секунд приходит provider message со статусом live и старым счётом. «Просто применить последнее сообщение» опасно: пользователь увидит откат завершённого матча. State machine должна отклонить переход и записать диагностический факт, а восстановление пройти через отдельную reconciliation-процедуру.
Полезно хранить причины переходов и version/sequence. Тогда расследование отвечает не только «какой статус сейчас», но и «почему система решила перейти сюда». EventNormalizer может сохранить исходную provider-семантику, а продуктовая модель — принять только разрешённый переход.
Contract tests здесь сильнее обычных snapshot tests: они прогоняют последовательности, включая дубли, запоздание и конфликт источников. Ошибка должна проявиться до того, как невозможная комбинация попадёт в UI.
Сверка состояния вместо ручного «подправили в базе»
Для долгоживущих событий нужен контролируемый reconciliation: получить authoritative snapshot у провайдера, сравнить с текущим состоянием и выполнить специальный переход с причиной. Так исправление остаётся наблюдаемым и не ломает историю.

Связанные разборы по той же архитектурной цепочке
Поток коэффициентов и событий: путь данных от поставщика до экрана
Из каких участков складывается доставка коэффициента в реальном времени и где появляются очереди, рассинхронизация и устаревшие данные.
Перейти к разбору →Интеграции данныхСопоставление спортивных событий между поставщиками данных
Как сопоставлять спортивные события разных поставщиков: внутренние идентификаторы, кандидаты, уверенность совпадения, конфликты и ручная проверка.
Перейти к разбору →АрхитектураАрхитектура потоковых спортивных данных: где теряется задержка
Как устроить путь спортивного события от поставщика до клиента и где измерять задержку, чтобы цифры действительно помогали архитектурным решениям.
Перейти к разбору →Букмекерская логикаКнопка коэффициента: доступность, изменение цены и приостановка
Как проектировать кнопку коэффициента: изменение цены, приостановка, фокус, клавиатурное управление и понятные состояния после выбора пользователя.
Перейти к разбору →