6.7 KiB
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
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
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
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)
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)
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
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
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
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.