Document tournament live websockets
This commit is contained in:
parent
3dcc9727a5
commit
4dc8198bc2
|
|
@ -32,6 +32,7 @@ Leader/follower tournament sync docs:
|
|||
- [doc/leader_follower.md](doc/leader_follower.md)
|
||||
- [doc/timer_api.md](doc/timer_api.md)
|
||||
- [doc/beamer_live_updates.md](doc/beamer_live_updates.md)
|
||||
- [doc/tournament_live_updates.md](doc/tournament_live_updates.md)
|
||||
|
||||
## Docker
|
||||
[Registry](https://gitlab.com/turniere/turniere-backend/container_registry)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,286 @@
|
|||
# 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:
|
||||
|
||||
1. Load initial state with HTTP.
|
||||
2. Subscribe to relevant ActionCable channel.
|
||||
3. Treat incoming websocket payloads as replacement snapshots.
|
||||
4. 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "TournamentChannel",
|
||||
"tournament_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
- teams
|
||||
- score rows
|
||||
|
||||
Subscription identifier for all matches:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "TournamentMatchesChannel",
|
||||
"tournament_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
Optional state filter:
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "TournamentMatchesChannel",
|
||||
"tournament_id": 123,
|
||||
"state": "in_progress"
|
||||
}
|
||||
```
|
||||
|
||||
Supported `state` values:
|
||||
|
||||
- omitted / `null`: all matches
|
||||
- `upcoming`
|
||||
- `single_team`
|
||||
- `not_ready`
|
||||
- `not_started`
|
||||
- `in_progress`
|
||||
- `finished`
|
||||
- `undecided`
|
||||
|
||||
Payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tournament_matches.updated",
|
||||
"tournament_id": 123,
|
||||
"state": null,
|
||||
"matches": [
|
||||
{
|
||||
"id": 456,
|
||||
"position": 0,
|
||||
"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/matches`
|
||||
- `GET /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/:id`
|
||||
- `PATCH /matches/:id`
|
||||
- `PATCH /matches/:id/swap`
|
||||
- `POST /groups/:group_id/matches`
|
||||
- `PATCH /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:
|
||||
|
||||
1. Fetch `GET /tournaments/:id`.
|
||||
2. Render tournament.
|
||||
3. Subscribe to `TournamentChannel`.
|
||||
4. On `tournament.updated`, replace tournament state with `payload.tournament`.
|
||||
5. Keep writes as HTTP requests.
|
||||
|
||||
Recommended match-list/beamer flow:
|
||||
|
||||
1. Fetch `GET /tournaments/:id/matches` or state-filtered matches endpoint.
|
||||
2. Render matches.
|
||||
3. Subscribe to `TournamentMatchesChannel` with same `state` filter.
|
||||
4. On `tournament_matches.updated`, replace that match list with `payload.matches`.
|
||||
5. 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`, `team.id`, and `match_score.id` as stable keys
|
||||
- tolerate duplicate payloads
|
||||
- tolerate fast sequences like score update followed by match finish
|
||||
|
||||
## ActionCable Message Example
|
||||
|
||||
Raw subscribe message:
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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 payloads
|
||||
- `TournamentBeamersChannel`: beamer display config payloads
|
||||
- `TournamentTeamActionListsChannel`: team action list payloads
|
||||
|
||||
Use `TournamentChannel` when full tournament state is needed.
|
||||
Use specialized channels when frontend wants smaller, focused payloads.
|
||||
|
||||
## Tests
|
||||
|
||||
Covered by:
|
||||
|
||||
- [spec/services/tournament_live_payload_spec.rb](../spec/services/tournament_live_payload_spec.rb)
|
||||
- [spec/e2e/http/tournament_live_websocket_spec.rb](../spec/e2e/http/tournament_live_websocket_spec.rb)
|
||||
- [spec/e2e/http/tournament_follow_sync_live_websocket_spec.rb](../spec/e2e/http/tournament_follow_sync_live_websocket_spec.rb)
|
||||
Loading…
Reference in New Issue