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
+121
View File
@@ -0,0 +1,121 @@
package ports
import (
"context"
"fmt"
"strings"
"time"
)
// UserLifecycleEventType identifies one supported user-lifecycle event
// kind propagated from User Service to Game Lobby through the
// `user:lifecycle_events` Redis Stream.
type UserLifecycleEventType string
const (
// UserLifecycleEventTypePermanentBlocked identifies the post-commit
// event emitted when `SanctionCodePermanentBlock` becomes active on
// an account.
UserLifecycleEventTypePermanentBlocked UserLifecycleEventType = "user.lifecycle.permanent_blocked"
// UserLifecycleEventTypeDeleted identifies the post-commit event
// emitted when `DeleteUser` soft-deletes an account.
UserLifecycleEventTypeDeleted UserLifecycleEventType = "user.lifecycle.deleted"
)
// String returns the wire value for eventType.
func (eventType UserLifecycleEventType) String() string {
return string(eventType)
}
// IsKnown reports whether eventType belongs to the frozen vocabulary.
func (eventType UserLifecycleEventType) IsKnown() bool {
switch eventType {
case UserLifecycleEventTypePermanentBlocked, UserLifecycleEventTypeDeleted:
return true
default:
return false
}
}
// UserLifecycleEvent stores the decoded shape of one entry from the
// `user:lifecycle_events` Redis Stream.
type UserLifecycleEvent struct {
// EntryID stores the Redis Streams entry id (`<ms>-<seq>` form). The
// consumer uses it as part of notification idempotency keys so a
// retried cascade publishes deterministically the same intent.
EntryID string
// EventType stores the frozen lifecycle event discriminator.
EventType UserLifecycleEventType
// UserID identifies the regular user whose lifecycle state changed.
UserID string
// OccurredAt stores the committed mutation timestamp emitted by User
// Service.
OccurredAt time.Time
// Source stores the machine-readable mutation source. always
// emits `admin_internal_api`.
Source string
// ActorType stores the audit actor type (e.g. `admin_user`,
// `system`).
ActorType string
// ActorID stores the optional audit actor identifier. It is empty
// when the upstream event carries no actor id.
ActorID string
// ReasonCode stores the committed `reason_code` emitted by User
// Service.
ReasonCode string
// TraceID stores the optional OpenTelemetry trace identifier
// propagated from the upstream request context.
TraceID string
}
// Validate reports whether event satisfies the structural invariants
// required for cascade processing.
func (event UserLifecycleEvent) Validate() error {
if strings.TrimSpace(event.EntryID) == "" {
return fmt.Errorf("user lifecycle event entry id must not be empty")
}
if !event.EventType.IsKnown() {
return fmt.Errorf("user lifecycle event type %q is unsupported", event.EventType)
}
if strings.TrimSpace(event.UserID) == "" {
return fmt.Errorf("user lifecycle event user id must not be empty")
}
if event.OccurredAt.IsZero() {
return fmt.Errorf("user lifecycle event occurred at must not be zero")
}
return nil
}
// UserLifecycleHandler processes one decoded lifecycle event. Returning
// nil advances the stream offset; returning a non-nil error holds the
// offset on the current entry so the consumer retries on the next loop
// iteration.
type UserLifecycleHandler func(ctx context.Context, event UserLifecycleEvent) error
// UserLifecycleConsumer drives the read loop over the
// `user:lifecycle_events` Redis Stream. The Redis adapter satisfies the
// interface in production; in-process stubs satisfy it in tests so the
// cascade worker can be exercised without spinning up Redis.
type UserLifecycleConsumer interface {
// OnEvent installs handler as the sole dispatcher for decoded events.
// A second call replaces the previous handler.
OnEvent(handler UserLifecycleHandler)
// Run drives the consumer loop until ctx is cancelled. The
// implementation is expected to be re-entrant only within a single
// goroutine.
Run(ctx context.Context) error
// Shutdown releases consumer-owned resources after Run has returned.
// The consumer must support being closed without an active Run loop.
Shutdown(ctx context.Context) error
}