SSE: событие партии несёт участников, клиент не дёргает лишних

Событие о партии рассылается всей группе, и клиент по нему инвалидировал историю
игр, публичные профили и личную статистику получателя. В группе из шести человек
любая партия двоих заставляла остальные четыре вкладки перезапрашивать свою
историю и открытый профиль, хотя у них ничего не изменилось.

Теперь событие несёт participant_ids. Общие витрины (карточка партии, списки и
статистика группы, топ и главная) обновляются у всех — рейтинг глобальный, чужая
партия действительно двигает топ. История, публичный профиль и личная статистика
обновляются только у тех, кто играл, и у зрителей их профилей.

При удалении партии участники собираются ДО удаления: каскад уносит их строки
вместе с партией, и собранный после список всегда был бы пустым. Это же
поведение закреплено тестом.

Событие без participant_ids (вкладка открыта до обновления сервера) обрабатывается
по-старому, широко: обновление бэкенда не ломает уже открытые страницы.

#33

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
This commit is contained in:
2026-09-09 20:47:32 +03:00
co-authored by Claude Opus 5
parent 4133c3c505
commit bdd07e8103
5 changed files with 145 additions and 11 deletions
+3 -1
View File
@@ -293,13 +293,15 @@ def delete_match(
admin: User = Depends(get_current_admin),
) -> s.OkResponse:
group_id = match_service.get_match(session, match_id).group_id # для уведомления
# До удаления: каскад унесёт участников вместе с партией.
participant_ids = notify.match_participant_ids(session, match_id)
admin_service.delete_match(session, match_id)
audit_service.record(
session, actor_id=admin.id, action="delete", entity_type="match", entity_id=match_id,
ip=client_ip(request),
)
session.commit()
notify.match_removed(session, match_id, group_id)
notify.match_removed(session, match_id, group_id, participant_ids)
return s.OkResponse()
+4 -1
View File
@@ -335,6 +335,9 @@ def delete_match(
match_service.assert_can_modify(session, match, user)
match_id_val = match.id
group_id_val = match.group_id
# Участников читаем до удаления: каскад унесёт их строки вместе с партией,
# а событию они нужны, чтобы клиент знал, чьи витрины протухли.
participant_ids = notify.match_participant_ids(session, match_id_val) # type: ignore[arg-type]
match_service.delete_match(session, match, expected_version=expected_version)
audit_service.record(
session,
@@ -347,5 +350,5 @@ def delete_match(
user_agent=request.headers.get("user-agent"),
)
session.commit()
notify.match_removed(session, match_id_val, group_id_val) # type: ignore[arg-type]
notify.match_removed(session, match_id_val, group_id_val, participant_ids) # type: ignore[arg-type]
return s.OkResponse()
+34 -6
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
from sqlmodel import Session, select
from app.core.events import hub
from app.models import GroupMember, Match
from app.models import GroupMember, Match, MatchParticipant
def _group_member_ids(session: Session, group_id: int) -> list[int]:
@@ -17,11 +17,29 @@ def _group_member_ids(session: Session, group_id: int) -> list[int]:
)
def match_participant_ids(session: Session, match_id: int) -> list[int]:
"""Кто играл в партии. Нужен в событии, чтобы клиент понимал, чьи витрины
(история игр, публичный профиль, личная статистика) реально протухли."""
return list(
session.exec(
select(MatchParticipant.user_id).where(MatchParticipant.match_id == match_id)
).all()
)
def match_changed(session: Session, match: Match) -> None:
"""Партия изменилась — уведомить всех участников её группы."""
"""Партия изменилась — уведомить всех участников её группы.
Адресат — вся группа: списки партий и статистика группы меняются у всех. А вот
история и профили протухают только у игравших, поэтому их id едут в событии."""
hub.publish(
_group_member_ids(session, match.group_id),
{"type": "match", "match_id": match.id, "group_id": match.group_id},
{
"type": "match",
"match_id": match.id,
"group_id": match.group_id,
"participant_ids": match_participant_ids(session, match.id), # type: ignore[arg-type]
},
)
@@ -35,11 +53,21 @@ def match_draft_changed(session: Session, match: Match, actor_id: int) -> None:
hub.publish(ids, {"type": "match_draft", "match_id": match.id, "group_id": match.group_id})
def match_removed(session: Session, match_id: int, group_id: int) -> None:
"""Партия удалена — уведомить участников группы (обновить списки)."""
def match_removed(
session: Session, match_id: int, group_id: int, participant_ids: list[int] | None = None
) -> None:
"""Партия удалена — уведомить участников группы (обновить списки).
participant_ids передаются снаружи: к этому моменту партии уже нет, а её участники
ушли каскадом, и собрать их из базы невозможно."""
hub.publish(
_group_member_ids(session, group_id),
{"type": "match", "match_id": match_id, "group_id": group_id},
{
"type": "match",
"match_id": match_id,
"group_id": group_id,
"participant_ids": participant_ids or [],
},
)