6.6 KiB
Tournament Live Updates
Tournament and match live updates use Rails ActionCable at /cable.
Use these channels for website pages and beamer pages that need automatic updates when:
- match scores change
- match state changes
- match positions change
- group scores change
- stages change
- playoff matches are populated
- follower-sync imports update a read-only follower tournament
This is websocket push, not webhook callback.
Transport
- websocket endpoint:
GET /cable - protocol: ActionCable
- no auth token is required for read subscriptions
- write operations still go through HTTP APIs
Frontend should:
- Load initial state with HTTP.
- Subscribe to relevant ActionCable channel.
- Treat incoming websocket payloads as replacement snapshots.
- Continue sending mutations through HTTP only.
Channels
TournamentChannel
Use this when page needs full tournament structure:
- stages
- groups
- group scores
- group matches
- playoff stages
- playoff matches
- teams and advancing flags
- timer fields
- team action lists
Subscription identifier:
{
"channel": "TournamentChannel",
"tournament_id": 123
}
Payload:
{
"type": "tournament.updated",
"tournament_id": 123,
"tournament": {
"id": 123,
"name": "Summer Cup",
"code": "abc123",
"public": true,
"description": "Example tournament",
"playoff_teams_amount": 4,
"instant_finalists_amount": 4,
"intermediate_round_participants_amount": 0,
"timestamp": null,
"timer_mode": null,
"owner_username": "owner",
"stages": [],
"teams": [],
"team_action_lists": []
}
}
The nested tournament object has same public shape as:
GET /tournaments/:id
Frontend can replace cached tournament state with payload.tournament.
TournamentMatchesChannel
Use this when page only needs match cards/list updates:
- match id
- position
- state
- group identity
- stage identity
- teams
- score rows
Subscription identifier for all matches:
{
"channel": "TournamentMatchesChannel",
"tournament_id": 123
}
Optional state filter:
{
"channel": "TournamentMatchesChannel",
"tournament_id": 123,
"state": "in_progress"
}
Supported state values:
- omitted /
null: all matches upcomingsingle_teamnot_readynot_startedin_progressfinishedundecided
Payload:
{
"type": "tournament_matches.updated",
"tournament_id": 123,
"state": null,
"matches": [
{
"id": 456,
"position": 0,
"state": "in_progress",
"group": {
"id": 44,
"number": 1
},
"stage": {
"id": 33,
"level": -1,
"state": "in_progress"
},
"teams": [
{
"id": 10,
"name": "Team A"
},
{
"id": 11,
"name": "Team B"
}
],
"match_scores": [
{
"id": 900,
"points": 12,
"hidden_points": 0,
"team": {
"id": 10,
"name": "Team A"
}
},
{
"id": 901,
"points": 8,
"hidden_points": 0,
"team": {
"id": 11,
"name": "Team B"
}
}
]
}
]
}
The matches array has same shape as:
GET /tournaments/:id/matchesGET /tournaments/:id/matches?state=:state
Frontend can replace cached match list for that filter with payload.matches.
Initial Snapshot
Both channels transmit current snapshot immediately after subscription.
This means frontend can safely:
- fetch over HTTP first, then subscribe
- or subscribe and use first websocket message as live refresh
HTTP first is still recommended because it gives normal request/error/loading behavior.
Broadcast Sources
Broadcasts happen after successful backend writes/imports.
Covered direct HTTP mutations:
PATCH /match_scores/:idPATCH /matches/:idPATCH /matches/:id/swapPOST /groups/:group_id/matchesPATCH /stages/:id
Covered follower-sync mutation:
PATCH /tournaments/:id/sync_state
Practical effects:
- score update broadcasts updated matches and updated group scores
- finishing group stage broadcasts new playoff stages/matches
- playoff match finish broadcasts populated next match
- follower tournament subscribers get same updates after source sync import
Frontend Integration
Recommended tournament page flow:
- Fetch
GET /tournaments/:id. - Render tournament.
- Subscribe to
TournamentChannel. - On
tournament.updated, replace tournament state withpayload.tournament. - Keep writes as HTTP requests.
Recommended match-list/beamer flow:
- Fetch
GET /tournaments/:id/matchesor state-filtered matches endpoint. - Render matches.
- Subscribe to
TournamentMatchesChannelwith samestatefilter. - On
tournament_matches.updated, replace that match list withpayload.matches. - For multiple tabs/lists, use one subscription per filter you need.
Recommended update handling:
- do not patch a single nested field from websocket payload
- replace the full snapshot for that channel/filter
- use
match.id,group.id,stage.id,team.id, andmatch_score.idas stable keys - tolerate duplicate payloads
- tolerate fast sequences like score update followed by match finish
ActionCable Message Example
Raw subscribe message:
{
"command": "subscribe",
"identifier": "{\"channel\":\"TournamentMatchesChannel\",\"tournament_id\":123,\"state\":\"in_progress\"}"
}
ActionCable wraps server payloads under message.
Client libraries usually unwrap this for you.
Raw received frame shape:
{
"identifier": "{\"channel\":\"TournamentMatchesChannel\",\"tournament_id\":123,\"state\":\"in_progress\"}",
"message": {
"type": "tournament_matches.updated",
"tournament_id": 123,
"state": "in_progress",
"matches": []
}
}
Relationship To Other Live Channels
Other specialized channels still exist:
TournamentTimersChannel: timer-only payloadsTournamentBeamersChannel: beamer display config payloadsTournamentTeamActionListsChannel: team action list payloads
Use TournamentChannel when full tournament state is needed.
Use specialized channels when frontend wants smaller, focused payloads.
Tests
Covered by: