122 lines
4.1 KiB
Go
122 lines
4.1 KiB
Go
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
|
|
}
|