feat: game lobby service
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user