feat: backend service
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user