feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
@@ -0,0 +1,99 @@
package ports
import (
"context"
"fmt"
"strings"
"time"
"galaxy/user/internal/domain/common"
)
// UserLifecycleEventType identifies one user-lifecycle event kind propagated
// to `Game Lobby` through the dedicated Redis Stream.
type UserLifecycleEventType string
const (
// UserLifecyclePermanentBlockedEventType identifies the post-commit event
// emitted when `SanctionCodePermanentBlock` becomes active on an account.
UserLifecyclePermanentBlockedEventType UserLifecycleEventType = "user.lifecycle.permanent_blocked"
// UserLifecycleDeletedEventType identifies the post-commit event emitted
// when a trusted `DeleteUser` command soft-deletes an account.
UserLifecycleDeletedEventType UserLifecycleEventType = "user.lifecycle.deleted"
)
// IsKnown reports whether the event type belongs to the frozen vocabulary.
func (eventType UserLifecycleEventType) IsKnown() bool {
switch eventType {
case UserLifecyclePermanentBlockedEventType, UserLifecycleDeletedEventType:
return true
default:
return false
}
}
// UserLifecycleEvent stores one post-commit user-lifecycle event envelope
// published to the `user:lifecycle_events` Redis Stream and consumed by
// `Game Lobby` for Race Name Directory cascade release.
type UserLifecycleEvent struct {
// EventType stores the frozen lifecycle event discriminator.
EventType UserLifecycleEventType
// UserID identifies the regular user whose lifecycle state changed.
UserID common.UserID
// OccurredAt stores the committed mutation timestamp.
OccurredAt time.Time
// Source stores the machine-readable mutation source. For Stage 22 this is
// always `admin_internal_api`.
Source common.Source
// Actor stores the audit actor metadata attached to the committed
// mutation.
Actor common.ActorRef
// ReasonCode stores the committed reason_code for the mutation.
ReasonCode common.ReasonCode
// TraceID stores the optional OpenTelemetry trace identifier propagated
// from the current request context.
TraceID string
}
// Validate reports whether event is structurally complete.
func (event UserLifecycleEvent) Validate() error {
if !event.EventType.IsKnown() {
return fmt.Errorf("user lifecycle event type %q is unsupported", event.EventType)
}
if err := event.UserID.Validate(); err != nil {
return fmt.Errorf("user lifecycle event user id: %w", err)
}
if err := common.ValidateTimestamp("user lifecycle event occurred at", event.OccurredAt); err != nil {
return err
}
if err := event.Source.Validate(); err != nil {
return fmt.Errorf("user lifecycle event source: %w", err)
}
if err := event.Actor.Validate(); err != nil {
return fmt.Errorf("user lifecycle event actor: %w", err)
}
if err := event.ReasonCode.Validate(); err != nil {
return fmt.Errorf("user lifecycle event reason code: %w", err)
}
if event.TraceID != "" && strings.TrimSpace(event.TraceID) != event.TraceID {
return fmt.Errorf("user lifecycle event trace id must not contain surrounding whitespace")
}
return nil
}
// UserLifecyclePublisher publishes one committed user-lifecycle event to the
// dedicated `user:lifecycle_events` Redis Stream.
type UserLifecyclePublisher interface {
// PublishUserLifecycleEvent propagates one committed lifecycle event. The
// implementation must validate the event envelope and perform exactly one
// idempotent append per call.
PublishUserLifecycleEvent(ctx context.Context, event UserLifecycleEvent) error
}