197 lines
6.7 KiB
Markdown
197 lines
6.7 KiB
Markdown
# Flows
|
|
|
|
This document collects the eight platform flows that span Game Lobby plus
|
|
its synchronous and asynchronous neighbours. Narrative descriptions of the
|
|
rules these flows enforce live in `../README.md`; the diagrams here focus on
|
|
the message order across the boundary.
|
|
|
|
## Public Game Application
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant Gateway
|
|
participant Lobby as Lobby publichttp
|
|
participant UserSvc as User Service
|
|
participant Redis
|
|
participant Stream as notification:intents
|
|
|
|
User->>Gateway: lobby.application.submit(game_id, race_name)
|
|
Gateway->>Lobby: POST /api/v1/lobby/games/{id}/applications + X-User-ID
|
|
Lobby->>UserSvc: GetEligibility(user_id)
|
|
UserSvc-->>Lobby: snapshot (entitlement, sanctions)
|
|
Lobby->>Redis: persist Application(submitted) + indexes
|
|
Lobby->>Stream: lobby.application.submitted (admin recipients)
|
|
Lobby-->>Gateway: 200 ApplicationRecord
|
|
```
|
|
|
|
Approval and rejection follow the same pattern, mutating the application
|
|
status to `approved`/`rejected` and emitting
|
|
`lobby.membership.approved`/`lobby.membership.rejected` to the applicant.
|
|
|
|
## Private Game Invite
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Owner
|
|
participant Invitee
|
|
participant Lobby
|
|
participant Redis
|
|
participant Stream as notification:intents
|
|
|
|
Owner->>Lobby: lobby.invite.create(invitee_user_id)
|
|
Lobby->>Redis: persist Invite(created)
|
|
Lobby->>Stream: lobby.invite.created (recipient: invitee)
|
|
|
|
Invitee->>Lobby: lobby.invite.redeem(race_name)
|
|
Lobby->>Lobby: User Service guard for inviter and invitee
|
|
Lobby->>Redis: RND.Reserve + Membership(active) + Invite(redeemed)
|
|
Lobby->>Stream: lobby.invite.redeemed (recipient: owner)
|
|
```
|
|
|
|
The owner-facing decline and revoke transitions persist the invite status
|
|
update and produce no notification in v1.
|
|
|
|
## Enrollment Automation
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Tick as Worker tick
|
|
participant Lobby
|
|
participant Redis
|
|
participant Stream as notification:intents
|
|
|
|
Tick->>Lobby: enrollment automation cycle
|
|
Lobby->>Redis: load enrollment_open games + roster sizes
|
|
alt deadline reached or gap exhausted
|
|
Lobby->>Redis: status enrollment_open → ready_to_start (CAS)
|
|
Lobby->>Redis: pending invites → expired
|
|
Lobby->>Stream: lobby.invite.expired (per expired invite)
|
|
else still within window
|
|
Lobby-->>Tick: no-op
|
|
end
|
|
```
|
|
|
|
Manual `lobby.game.ready_to_start` from owner or admin runs the same close
|
|
pipeline synchronously without waiting for the next tick.
|
|
|
|
## Game Start (happy path)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Actor as Owner or Admin
|
|
participant Lobby
|
|
participant Redis
|
|
participant RT as Runtime Manager
|
|
participant GM as Game Master
|
|
|
|
Actor->>Lobby: lobby.game.start
|
|
Lobby->>Redis: status ready_to_start → starting (CAS)
|
|
Lobby->>Redis: XADD runtime:start_jobs
|
|
RT->>Redis: XADD runtime:job_results (success + container metadata)
|
|
Lobby->>Redis: persist runtime_binding on game record
|
|
Lobby->>GM: POST /internal/games/{id}/register-runtime
|
|
GM-->>Lobby: 200 OK
|
|
Lobby->>Redis: status starting → running; set started_at
|
|
```
|
|
|
|
If runtime metadata persistence fails, Lobby publishes a stop-job to remove
|
|
the orphan container before flipping the game to `start_failed`.
|
|
|
|
## Game Start (GM unavailable)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Lobby
|
|
participant Redis
|
|
participant GM as Game Master
|
|
participant Stream as notification:intents
|
|
|
|
Lobby->>GM: POST /internal/games/{id}/register-runtime
|
|
GM-->>Lobby: timeout / 5xx
|
|
Lobby->>Redis: status starting → paused (CAS)
|
|
Lobby->>Stream: lobby.runtime_paused_after_start (admin)
|
|
Note over Lobby,GM: Container stays alive; admin restarts GM<br/>and issues lobby.game.resume.
|
|
```
|
|
|
|
## Game Finish + Capability Evaluation
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant GM as Game Master
|
|
participant Stream as gm:lobby_events
|
|
participant Lobby
|
|
participant Redis
|
|
participant Intents as notification:intents
|
|
|
|
GM->>Stream: XADD runtime_snapshot_update (player_turn_stats)
|
|
Lobby->>Redis: UpdateMax for each member's stats aggregate
|
|
GM->>Stream: XADD game_finished
|
|
Lobby->>Redis: status running/paused → finished; finished_at = event_ts
|
|
Lobby->>Redis: capability evaluator runs per active membership
|
|
alt member capable
|
|
Lobby->>Redis: RND.MarkPendingRegistration(eligible_until = finished_at + 30d)
|
|
Lobby->>Intents: lobby.race_name.registration_eligible (recipient: user)
|
|
else not capable
|
|
Lobby->>Redis: RND.ReleaseReservation
|
|
Lobby->>Intents: lobby.race_name.registration_denied (optional)
|
|
end
|
|
Lobby->>Redis: ReleaseReservation for removed/blocked memberships
|
|
Lobby->>Redis: delete per-game stats aggregate
|
|
```
|
|
|
|
The evaluation guard `lobby:capability_evaluation:done:<game_id>` makes a
|
|
replayed `game_finished` event a no-op.
|
|
|
|
## Race Name Registration
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant Lobby
|
|
participant UserSvc as User Service
|
|
participant RND as Race Name Directory
|
|
participant Stream as notification:intents
|
|
|
|
User->>Lobby: lobby.race_name.register(race_name)
|
|
Lobby->>UserSvc: GetEligibility (sanctions, max_registered_race_names)
|
|
UserSvc-->>Lobby: snapshot
|
|
Lobby->>RND: Register(game_id, user_id, race_name)
|
|
RND-->>Lobby: ok / ErrPendingExpired / ErrQuotaExceeded
|
|
alt success
|
|
Lobby->>Stream: lobby.race_name.registered (recipient: user)
|
|
Lobby-->>User: 200 RegisteredRaceName
|
|
else precondition failure
|
|
Lobby-->>User: 422 DomainPreconditionError
|
|
end
|
|
```
|
|
|
|
Registration consumes one tariff slot keyed by `(canonical_key, user_id)`;
|
|
tariff downgrade never revokes existing registrations.
|
|
|
|
## Cascade Release on User Lifecycle Event
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant US as User Service
|
|
participant Stream as user:lifecycle_events
|
|
participant Lobby
|
|
participant RT as Runtime Manager
|
|
participant Intents as notification:intents
|
|
|
|
US->>Stream: XADD permanent_blocked or deleted
|
|
Lobby->>Stream: XREAD (consumer)
|
|
Lobby->>Lobby: RND.ReleaseAllByUser
|
|
Lobby->>Lobby: memberships → blocked + lobby.membership.blocked per private game
|
|
Lobby->>Lobby: applications → rejected
|
|
Lobby->>Lobby: invites (addressed and inviter-side) → revoked
|
|
Lobby->>Lobby: owned non-terminal games → cancelled (external_block trigger)
|
|
Lobby->>RT: XADD runtime:stop_jobs for in-flight owned games
|
|
Lobby->>Intents: lobby.membership.blocked per affected membership
|
|
Lobby->>Stream: advance offset
|
|
```
|
|
|
|
Every step is idempotent at the store layer (`ErrConflict` from a CAS is
|
|
treated as «already done»); the consumer only advances the offset once the
|
|
handler returns nil.
|