Files
2026-05-06 10:14:55 +03:00

82 lines
2.6 KiB
Go

package lobby
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"go.uber.org/zap"
)
// OnUserBlocked releases every lobby binding owned by the user under
// the `blocked` semantics: active memberships flip to `blocked`,
// pending applications get rejected, pending invites incoming get
// declined / outgoing get revoked, race-name entries are deleted, and
// owned games in non-running statuses are cancelled.
//
// Implements `internal/user.LobbyCascade.OnUserBlocked`. Errors during
// the cascade are joined and returned but never roll back the
// already-committed user write — the canonical state is the row in
// Postgres.
func (s *Service) OnUserBlocked(ctx context.Context, userID uuid.UUID) error {
return s.runCascade(ctx, userID, MembershipStatusBlocked)
}
// OnUserDeleted runs the same cascade as OnUserBlocked but transitions
// memberships to `removed` instead of `blocked`. Implements
// `internal/user.LobbyCascade.OnUserDeleted`.
func (s *Service) OnUserDeleted(ctx context.Context, userID uuid.UUID) error {
return s.runCascade(ctx, userID, MembershipStatusRemoved)
}
func (s *Service) runCascade(ctx context.Context, userID uuid.UUID, membershipStatus string) error {
snap, err := s.deps.Store.LoadCascadeSnapshot(ctx, userID)
if err != nil {
return fmt.Errorf("lobby cascade: load snapshot: %w", err)
}
if snap.empty() {
return nil
}
now := s.deps.Now().UTC()
if err := s.deps.Store.CascadeUser(ctx, userID, snap, membershipStatus, now); err != nil {
return fmt.Errorf("lobby cascade: write: %w", err)
}
s.deps.Cache.EvictUserMemberships(userID)
s.deps.Cache.EvictUserRaceNames(userID)
s.deps.Cache.EvictOwnerGames(userID)
var notifyErrs []error
for _, gameID := range snap.OwnedGameIDs {
s.deps.Cache.RemoveGame(gameID)
}
if len(snap.ActiveMembershipIDs) > 0 {
intent := LobbyNotification{
Kind: NotificationLobbyMembershipRemoved,
IdempotencyKey: "user-cascade-membership:" + userID.String(),
Recipients: []uuid.UUID{userID},
Payload: map[string]any{
"reason": membershipStatus,
},
}
if pubErr := s.deps.Notification.PublishLobbyEvent(ctx, intent); pubErr != nil {
notifyErrs = append(notifyErrs, pubErr)
}
}
if len(notifyErrs) > 0 {
s.deps.Logger.Warn("lobby cascade notification failures",
zap.String("user_id", userID.String()),
zap.Int("notify_errors", len(notifyErrs)))
}
return errors.Join(notifyErrs...)
}
func (snap CascadeUserSnapshot) empty() bool {
return len(snap.OwnedGameIDs) == 0 &&
len(snap.ActiveMembershipIDs) == 0 &&
len(snap.PendingApplications) == 0 &&
len(snap.IncomingInvites) == 0 &&
len(snap.OutgoingInvites) == 0 &&
len(snap.RaceNameKeys) == 0
}