228 lines
9.6 KiB
Go
228 lines
9.6 KiB
Go
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.
|
|
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 {
|
|
return defaultPrefix + "gap_activated_at:" + encodeKeyComponent(gameID.String())
|
|
}
|
|
|
|
// StreamOffset returns the Redis key that stores the last successfully
|
|
// processed entry id for one Redis Stream consumer. The streamLabel is
|
|
// the short logical identifier of the consumer (e.g. `runtime_results`,
|
|
// `gm_events`, `user_lifecycle`), not the full stream name; it stays
|
|
// stable when the underlying stream key is renamed.
|
|
func (Keyspace) StreamOffset(streamLabel string) string {
|
|
return defaultPrefix + "stream_offsets:" + encodeKeyComponent(streamLabel)
|
|
}
|
|
|
|
// GameTurnStat returns the per-user Redis key that stores the
|
|
// initial/max stats aggregate for one game. keeps one key per
|
|
// user so the Lua-backed SaveInitial and UpdateMax scripts can operate
|
|
// on a single primary key without a secondary index.
|
|
func (Keyspace) GameTurnStat(gameID common.GameID, userID string) string {
|
|
return defaultPrefix + "game_turn_stats:" +
|
|
encodeKeyComponent(gameID.String()) + ":" +
|
|
encodeKeyComponent(userID)
|
|
}
|
|
|
|
// GameTurnStatsByGame returns the set key that stores every userID for
|
|
// which a GameTurnStat key exists for gameID. The set is the lookup
|
|
// index used by Load and Delete so they avoid a Redis SCAN over the
|
|
// whole keyspace.
|
|
func (Keyspace) GameTurnStatsByGame(gameID common.GameID) string {
|
|
return defaultPrefix + "game_turn_stats_by_game:" +
|
|
encodeKeyComponent(gameID.String())
|
|
}
|
|
|
|
// CapabilityEvaluationGuard returns the Redis key whose presence marks
|
|
// gameID as already evaluated by the The capability evaluator
|
|
// uses SETNX on this key to make replayed `game_finished` events safe.
|
|
func (Keyspace) CapabilityEvaluationGuard(gameID common.GameID) string {
|
|
return defaultPrefix + "capability_evaluation:done:" +
|
|
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))
|
|
}
|