239 lines
9.7 KiB
Go
239 lines
9.7 KiB
Go
package ports
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// Kind classifiers for RaceNameDirectory bindings.
|
|
const (
|
|
// KindRegistered identifies a permanent user-owned race name in the
|
|
// Race Name Directory.
|
|
KindRegistered = "registered"
|
|
|
|
// KindReservation identifies a per-game race name binding created at
|
|
// application approval or invite redeem.
|
|
KindReservation = "reservation"
|
|
|
|
// KindPendingRegistration identifies a reservation that survived a
|
|
// capable game finish and is now waiting for lobby.race_name.register
|
|
// within its 30-day window.
|
|
KindPendingRegistration = "pending_registration"
|
|
)
|
|
|
|
// Sentinel errors returned by RaceNameDirectory implementations. Callers
|
|
// map them to stable lobby error codes (name_taken, invalid_request,
|
|
// race_name_pending_window_expired, race_name_registration_quota_exceeded).
|
|
var (
|
|
// ErrNameTaken is returned when raceName is already held by a different
|
|
// user as registered, active reservation, or pending_registration on
|
|
// the same canonical key.
|
|
ErrNameTaken = errors.New("race name is already taken")
|
|
|
|
// ErrInvalidName is returned when raceName fails character-set
|
|
// validation, when the canonical key cannot be derived, or when a
|
|
// subsequent MarkPendingRegistration is invoked with a different
|
|
// eligibleUntil than the one already stored.
|
|
ErrInvalidName = errors.New("race name is invalid")
|
|
|
|
// ErrPendingMissing is returned by Register when no pending
|
|
// registration exists for the supplied (game_id, user_id, race_name)
|
|
// tuple.
|
|
ErrPendingMissing = errors.New("pending race-name registration missing")
|
|
|
|
// ErrPendingExpired is returned by Register when the pending
|
|
// registration exists but its eligible_until has passed.
|
|
ErrPendingExpired = errors.New("pending race-name registration expired")
|
|
|
|
// ErrQuotaExceeded is reserved for callers that enforce the
|
|
// max_registered_race_names limit before invoking Register; directory
|
|
// implementations do not raise it directly.
|
|
ErrQuotaExceeded = errors.New("race name registration quota exceeded")
|
|
)
|
|
|
|
// Availability reports whether raceName is taken for the acting user and,
|
|
// when taken, who holds it and under which kind of binding.
|
|
type Availability struct {
|
|
// Taken is true when any binding on the canonical key belongs to a
|
|
// user different from the actor.
|
|
Taken bool
|
|
|
|
// HolderUserID carries the owning user id of the strongest existing
|
|
// binding on the canonical key, or the empty string when no binding
|
|
// exists.
|
|
HolderUserID string
|
|
|
|
// Kind identifies the strongest existing binding on the canonical key
|
|
// as one of KindRegistered, KindPendingRegistration, KindReservation,
|
|
// or the empty string when no binding exists.
|
|
Kind string
|
|
}
|
|
|
|
// RegisteredName describes one registered race name owned by a user.
|
|
type RegisteredName struct {
|
|
// CanonicalKey is the policy-produced uniqueness key.
|
|
CanonicalKey string
|
|
|
|
// RaceName is the original-casing user-submitted display form.
|
|
RaceName string
|
|
|
|
// SourceGameID is the game whose capable finish produced the pending
|
|
// entry that was converted into this registration.
|
|
SourceGameID string
|
|
|
|
// RegisteredAtMs is the Unix-milliseconds timestamp of the successful
|
|
// Register call.
|
|
RegisteredAtMs int64
|
|
}
|
|
|
|
// Reservation describes one active per-game race name reservation owned by
|
|
// a user. Reservations with status pending_registration are returned by
|
|
// ListPendingRegistrations instead.
|
|
type Reservation struct {
|
|
// CanonicalKey is the policy-produced uniqueness key.
|
|
CanonicalKey string
|
|
|
|
// RaceName is the original-casing user-submitted display form.
|
|
RaceName string
|
|
|
|
// GameID is the game hosting the reservation.
|
|
GameID string
|
|
|
|
// ReservedAtMs is the Unix-milliseconds timestamp of the first
|
|
// successful Reserve call for the tuple.
|
|
ReservedAtMs int64
|
|
}
|
|
|
|
// PendingRegistration describes one reservation that was promoted to
|
|
// pending_registration at a capable game finish.
|
|
type PendingRegistration struct {
|
|
// CanonicalKey is the policy-produced uniqueness key.
|
|
CanonicalKey string
|
|
|
|
// RaceName is the original-casing user-submitted display form.
|
|
RaceName string
|
|
|
|
// GameID is the source game whose capable finish produced the pending
|
|
// entry.
|
|
GameID string
|
|
|
|
// ReservedAtMs is the Unix-milliseconds timestamp of the original
|
|
// Reserve call; it is preserved across the pending-registration
|
|
// promotion.
|
|
ReservedAtMs int64
|
|
|
|
// EligibleUntilMs is the Unix-milliseconds deadline for an eligible
|
|
// Register call. After this moment ExpirePendingRegistrations releases
|
|
// the entry.
|
|
EligibleUntilMs int64
|
|
}
|
|
|
|
// ExpiredPending describes one pending registration released by a single
|
|
// ExpirePendingRegistrations pass. The slice lets callers emit telemetry
|
|
// and cascade release side effects without a second round-trip.
|
|
type ExpiredPending struct {
|
|
// CanonicalKey is the policy-produced uniqueness key.
|
|
CanonicalKey string
|
|
|
|
// RaceName is the original-casing user-submitted display form.
|
|
RaceName string
|
|
|
|
// GameID is the source game that produced the pending entry.
|
|
GameID string
|
|
|
|
// UserID is the holder of the released pending entry.
|
|
UserID string
|
|
|
|
// EligibleUntilMs is the Unix-milliseconds deadline that lapsed.
|
|
EligibleUntilMs int64
|
|
}
|
|
|
|
// RaceNameDirectory is the platform source of truth for in-game race_name
|
|
// values across three levels of state: registered (permanent, one per
|
|
// user), reservation (per-game holding), and pending_registration (30-day
|
|
// post-capable-finish window). It owns canonical-key derivation through
|
|
// the lobby/internal/domain/racename policy and arbitrates platform-wide
|
|
// uniqueness so that a name considered taken for one user remains
|
|
// exclusively bound to that user across registered, active reservations,
|
|
// and pending registrations sharing the same canonical key.
|
|
//
|
|
// One user may hold the same canonical key concurrently across multiple
|
|
// active games; cross-user conflicts surface as ErrNameTaken.
|
|
type RaceNameDirectory interface {
|
|
// Canonicalize normalizes raceName through the lobby RND policy
|
|
// (character-set validation, Unicode case fold, frozen confusable-pair
|
|
// map). Invalid raceName values surface as ErrInvalidName.
|
|
Canonicalize(raceName string) (canonical string, err error)
|
|
|
|
// Check reports whether raceName is taken for actorUserID on its
|
|
// canonical key. Taken is false when no binding exists or when the
|
|
// existing binding is owned by actorUserID; HolderUserID and Kind
|
|
// carry the existing binding's metadata regardless of the Taken value.
|
|
// A concurrent Reserve may race against a Check result, so service
|
|
// code that needs atomicity must rely on Reserve returning
|
|
// ErrNameTaken rather than pre-checking.
|
|
Check(ctx context.Context, raceName, actorUserID string) (Availability, error)
|
|
|
|
// Reserve claims raceName for (gameID, userID). A second call by the
|
|
// same holder for the same tuple is idempotent and returns nil.
|
|
// ErrNameTaken is returned when any registered, reservation, or
|
|
// pending_registration binding on the canonical key is owned by a
|
|
// different user (in any game).
|
|
Reserve(ctx context.Context, gameID, userID, raceName string) error
|
|
|
|
// ReleaseReservation removes the reservation held by userID for
|
|
// raceName in gameID. It is a no-op when no reservation exists for
|
|
// the tuple, when the reservation is held by a different user, and
|
|
// when raceName fails validation; none of these cases surface an
|
|
// error. Defensive release paths rely on these semantics.
|
|
ReleaseReservation(ctx context.Context, gameID, userID, raceName string) error
|
|
|
|
// MarkPendingRegistration promotes the reservation stored for
|
|
// (gameID, userID) on raceName's canonical key to
|
|
// pending_registration status with the supplied eligibleUntil. A
|
|
// second call with the same eligibleUntil is a no-op; a call with a
|
|
// different eligibleUntil returns ErrInvalidName. Callers must
|
|
// ensure the underlying reservation exists.
|
|
MarkPendingRegistration(
|
|
ctx context.Context,
|
|
gameID, userID, raceName string,
|
|
eligibleUntil time.Time,
|
|
) error
|
|
|
|
// ExpirePendingRegistrations releases every pending registration whose
|
|
// eligibleUntil is at or before now and returns the released entries
|
|
// in a single slice so callers can emit metrics or notifications.
|
|
// Running twice over the same state returns an empty slice the second
|
|
// time.
|
|
ExpirePendingRegistrations(ctx context.Context, now time.Time) ([]ExpiredPending, error)
|
|
|
|
// Register converts the pending registration identified by (gameID,
|
|
// userID) on raceName's canonical key into a registered race name.
|
|
// Missing pending returns ErrPendingMissing; expired pending returns
|
|
// ErrPendingExpired. A second call after a successful registration
|
|
// for the same tuple is idempotent and returns nil. Quota enforcement
|
|
// is the caller's responsibility.
|
|
Register(ctx context.Context, gameID, userID, raceName string) error
|
|
|
|
// ListRegistered returns every registered race name owned by userID.
|
|
// The ordering is implementation-defined.
|
|
ListRegistered(ctx context.Context, userID string) ([]RegisteredName, error)
|
|
|
|
// ListPendingRegistrations returns every pending registration owned by
|
|
// userID. The ordering is implementation-defined.
|
|
ListPendingRegistrations(ctx context.Context, userID string) ([]PendingRegistration, error)
|
|
|
|
// ListReservations returns every active reservation owned by userID
|
|
// whose status has not yet been promoted to pending_registration. The
|
|
// ordering is implementation-defined.
|
|
ListReservations(ctx context.Context, userID string) ([]Reservation, error)
|
|
|
|
// ReleaseAllByUser atomically clears every registered, reservation,
|
|
// and pending_registration binding owned by userID. It is invoked by
|
|
// the user lifecycle consumer on permanent_blocked and deleted events.
|
|
// Running twice over the same state is safe and produces no
|
|
// additional side effects.
|
|
ReleaseAllByUser(ctx context.Context, userID string) error
|
|
}
|