# 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
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:` 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.