feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
+9 -168
View File
@@ -2,178 +2,25 @@ package redisstate
import (
"encoding/base64"
"time"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/domain/game"
"galaxy/lobby/internal/domain/racename"
)
// defaultPrefix is the mandatory `lobby:` namespace prefix shared by every
// Game Lobby Redis key.
const defaultPrefix = "lobby:"
// GameRecordTTL is the Redis retention applied to game records. The
// value is zero (no expiry); a future stage will revisit this
// choice when the platform locks in archival/GDPR policy.
const GameRecordTTL time.Duration = 0
// ApplicationRecordTTL is the Redis retention applied to application
// records. uses zero (no expiry) to match game records; the
// archival policy will be revisited when the platform locks it in.
const ApplicationRecordTTL time.Duration = 0
// InviteRecordTTL is the Redis retention applied to invite records.
// uses zero (no expiry); the `expires_at` field is a business
// deadline enforced by the service layer, not a Redis TTL.
const InviteRecordTTL time.Duration = 0
// MembershipRecordTTL is the Redis retention applied to membership
// records. uses zero (no expiry) to match the other participant
// entities.
const MembershipRecordTTL time.Duration = 0
// Keyspace builds the frozen Game Lobby Redis keys. All dynamic key
// segments are encoded with base64url so raw key structure does not
// depend on user-provided or caller-provided characters.
// Keyspace builds the Game Lobby Redis keys that survive the PG_PLAN.md
// §6A and §6B migrations: per-game ephemeral runtime aggregates,
// capability-evaluation guards, gap activation timestamps, and stream
// consumer offsets. The four core enrollment entities (game, application,
// invite, membership) and the Race Name Directory live in PostgreSQL —
// their previous keyspace methods are gone.
//
// All dynamic key segments are encoded with base64url so raw key structure
// does not depend on user-provided or caller-provided characters.
type Keyspace struct{}
// Game returns the primary Redis key for one game record.
func (Keyspace) Game(gameID common.GameID) string {
return defaultPrefix + "games:" + encodeKeyComponent(gameID.String())
}
// GamesByStatus returns the sorted-set key that stores game identifiers
// indexed by their current status.
func (Keyspace) GamesByStatus(status game.Status) string {
return defaultPrefix + "games_by_status:" + encodeKeyComponent(string(status))
}
// GamesByOwner returns the set key that stores game identifiers owned
// by one user. The set is maintained for private games whose
// OwnerUserID is non-empty (public games are admin-owned and carry an
// empty OwnerUserID, so they never enter the index).
func (Keyspace) GamesByOwner(userID string) string {
return defaultPrefix + "games_by_owner:" + encodeKeyComponent(userID)
}
// Application returns the primary Redis key for one application record.
func (Keyspace) Application(applicationID common.ApplicationID) string {
return defaultPrefix + "applications:" + encodeKeyComponent(applicationID.String())
}
// ApplicationsByGame returns the set key that stores application
// identifiers attached to one game.
func (Keyspace) ApplicationsByGame(gameID common.GameID) string {
return defaultPrefix + "game_applications:" + encodeKeyComponent(gameID.String())
}
// ApplicationsByUser returns the set key that stores application
// identifiers submitted by one applicant.
func (Keyspace) ApplicationsByUser(applicantUserID string) string {
return defaultPrefix + "user_applications:" + encodeKeyComponent(applicantUserID)
}
// UserGameApplication returns the lookup key that stores the single
// non-rejected application identifier for one (user, game) pair. Presence
// of this key blocks a second submitted/approved application for the
// same user and game.
func (Keyspace) UserGameApplication(applicantUserID string, gameID common.GameID) string {
return defaultPrefix + "user_game_application:" +
encodeKeyComponent(applicantUserID) + ":" +
encodeKeyComponent(gameID.String())
}
// Invite returns the primary Redis key for one invite record.
func (Keyspace) Invite(inviteID common.InviteID) string {
return defaultPrefix + "invites:" + encodeKeyComponent(inviteID.String())
}
// InvitesByGame returns the set key that stores invite identifiers
// attached to one game.
func (Keyspace) InvitesByGame(gameID common.GameID) string {
return defaultPrefix + "game_invites:" + encodeKeyComponent(gameID.String())
}
// InvitesByUser returns the set key that stores invite identifiers
// addressed to one invitee.
func (Keyspace) InvitesByUser(inviteeUserID string) string {
return defaultPrefix + "user_invites:" + encodeKeyComponent(inviteeUserID)
}
// InvitesByInviter returns the set key that stores invite identifiers
// created by one inviter (private-game owner). The set retains
// invite_ids regardless of subsequent status transitions; callers
// filter by status when needed.
func (Keyspace) InvitesByInviter(inviterUserID string) string {
return defaultPrefix + "user_inviter_invites:" + encodeKeyComponent(inviterUserID)
}
// Membership returns the primary Redis key for one membership record.
func (Keyspace) Membership(membershipID common.MembershipID) string {
return defaultPrefix + "memberships:" + encodeKeyComponent(membershipID.String())
}
// MembershipsByGame returns the set key that stores membership
// identifiers attached to one game.
func (Keyspace) MembershipsByGame(gameID common.GameID) string {
return defaultPrefix + "game_memberships:" + encodeKeyComponent(gameID.String())
}
// MembershipsByUser returns the set key that stores membership
// identifiers held by one user.
func (Keyspace) MembershipsByUser(userID string) string {
return defaultPrefix + "user_memberships:" + encodeKeyComponent(userID)
}
// RegisteredRaceName returns the Redis key that stores the registered
// race name bound to canonical.
func (Keyspace) RegisteredRaceName(canonical racename.CanonicalKey) string {
return defaultPrefix + "race_names:registered:" + encodeKeyComponent(canonical.String())
}
// UserRegisteredRaceNames returns the set key that stores canonical keys
// of every registered race name owned by userID.
func (Keyspace) UserRegisteredRaceNames(userID string) string {
return defaultPrefix + "race_names:user_registered:" + encodeKeyComponent(userID)
}
// RaceNameReservation returns the Redis key that stores the per-game race
// name reservation bound to (gameID, canonical).
func (Keyspace) RaceNameReservation(gameID common.GameID, canonical racename.CanonicalKey) string {
return defaultPrefix + "race_names:reservations:" +
encodeKeyComponent(gameID.String()) + ":" +
encodeKeyComponent(canonical.String())
}
// UserRaceNameReservations returns the set key that stores
// `<encodedGameID>:<encodedCanonical>` tuples of every active reservation
// (including pending_registration) owned by userID.
func (Keyspace) UserRaceNameReservations(userID string) string {
return defaultPrefix + "race_names:user_reservations:" + encodeKeyComponent(userID)
}
// RaceNameCanonicalLookup returns the Redis key that stores the eager
// canonical-lookup cache entry for canonical. The cache surfaces the
// strongest existing binding (registered > pending_registration >
// reservation) so Check remains an O(1) read.
func (Keyspace) RaceNameCanonicalLookup(canonical racename.CanonicalKey) string {
return defaultPrefix + "race_names:canonical_lookup:" + encodeKeyComponent(canonical.String())
}
// PendingRaceNameIndex returns the singleton sorted-set key that indexes
// pending registrations by eligible_until_ms for the expiration worker.
func (Keyspace) PendingRaceNameIndex() string {
return defaultPrefix + "race_names:pending_index"
}
// RaceNameReservationMember returns the canonical member representation
// stored inside UserRaceNameReservations and PendingRaceNameIndex for
// (gameID, canonical).
func (Keyspace) RaceNameReservationMember(gameID common.GameID, canonical racename.CanonicalKey) string {
return encodeKeyComponent(gameID.String()) + ":" + encodeKeyComponent(canonical.String())
}
// GapActivatedAt returns the Redis key that stores the gap-window
// activation timestamp for one game.
func (Keyspace) GapActivatedAt(gameID common.GameID) string {
@@ -216,12 +63,6 @@ func (Keyspace) CapabilityEvaluationGuard(gameID common.GameID) string {
encodeKeyComponent(gameID.String())
}
// CreatedAtScore returns the frozen sorted-set score representation for
// game creation timestamps stored in the status index.
func CreatedAtScore(createdAt time.Time) float64 {
return float64(createdAt.UTC().UnixMilli())
}
func encodeKeyComponent(value string) string {
return base64.RawURLEncoding.EncodeToString([]byte(value))
}