feat: game lobby service
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package redisstate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
"galaxy/lobby/internal/ports"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// GapActivationRecordTTL is the Redis retention applied to gap activation
|
||||
// timestamps. uses zero (no expiry); the worker that consumes
|
||||
// these records will revisit retention when the surface
|
||||
// stabilizes.
|
||||
const GapActivationRecordTTL time.Duration = 0
|
||||
|
||||
// gapActivationRecord stores the strict Redis JSON shape used for one
|
||||
// gap-window activation timestamp.
|
||||
type gapActivationRecord struct {
|
||||
ActivatedAtMS int64 `json:"activated_at_ms"`
|
||||
}
|
||||
|
||||
// GapActivationStore provides Redis-backed durable storage for gap-window
|
||||
// activation timestamps used by enrollment automation.
|
||||
type GapActivationStore struct {
|
||||
client *redis.Client
|
||||
keys Keyspace
|
||||
}
|
||||
|
||||
// NewGapActivationStore constructs one Redis-backed gap activation store.
|
||||
// It returns an error when client is nil.
|
||||
func NewGapActivationStore(client *redis.Client) (*GapActivationStore, error) {
|
||||
if client == nil {
|
||||
return nil, errors.New("new gap activation store: nil redis client")
|
||||
}
|
||||
return &GapActivationStore{client: client, keys: Keyspace{}}, nil
|
||||
}
|
||||
|
||||
// MarkActivated writes at as the gap activation timestamp for gameID iff
|
||||
// no prior activation exists. A second call is a silent no-op.
|
||||
func (store *GapActivationStore) MarkActivated(ctx context.Context, gameID common.GameID, at time.Time) error {
|
||||
if store == nil || store.client == nil {
|
||||
return errors.New("mark gap activation: nil store")
|
||||
}
|
||||
if ctx == nil {
|
||||
return errors.New("mark gap activation: nil context")
|
||||
}
|
||||
if err := gameID.Validate(); err != nil {
|
||||
return fmt.Errorf("mark gap activation: %w", err)
|
||||
}
|
||||
if at.IsZero() {
|
||||
return errors.New("mark gap activation: at must not be zero")
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(gapActivationRecord{ActivatedAtMS: at.UTC().UnixMilli()})
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark gap activation: %w", err)
|
||||
}
|
||||
|
||||
args := redis.SetArgs{Mode: "NX"}
|
||||
if GapActivationRecordTTL > 0 {
|
||||
args.TTL = GapActivationRecordTTL
|
||||
}
|
||||
if _, err := store.client.SetArgs(ctx, store.keys.GapActivatedAt(gameID), payload, args).Result(); err != nil && !errors.Is(err, redis.Nil) {
|
||||
return fmt.Errorf("mark gap activation: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the gap-window activation time previously recorded for
|
||||
// gameID. The second return value is false when no activation has been
|
||||
// recorded.
|
||||
func (store *GapActivationStore) Get(ctx context.Context, gameID common.GameID) (time.Time, bool, error) {
|
||||
if store == nil || store.client == nil {
|
||||
return time.Time{}, false, errors.New("get gap activation: nil store")
|
||||
}
|
||||
if ctx == nil {
|
||||
return time.Time{}, false, errors.New("get gap activation: nil context")
|
||||
}
|
||||
if err := gameID.Validate(); err != nil {
|
||||
return time.Time{}, false, fmt.Errorf("get gap activation: %w", err)
|
||||
}
|
||||
|
||||
raw, err := store.client.Get(ctx, store.keys.GapActivatedAt(gameID)).Bytes()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return time.Time{}, false, nil
|
||||
}
|
||||
return time.Time{}, false, fmt.Errorf("get gap activation: %w", err)
|
||||
}
|
||||
|
||||
var record gapActivationRecord
|
||||
if err := json.Unmarshal(raw, &record); err != nil {
|
||||
return time.Time{}, false, fmt.Errorf("get gap activation: %w", err)
|
||||
}
|
||||
if record.ActivatedAtMS <= 0 {
|
||||
return time.Time{}, false, fmt.Errorf("get gap activation: activated_at_ms %d must be positive", record.ActivatedAtMS)
|
||||
}
|
||||
return time.UnixMilli(record.ActivatedAtMS).UTC(), true, nil
|
||||
}
|
||||
|
||||
// Compile-time interface assertion.
|
||||
var _ ports.GapActivationStore = (*GapActivationStore)(nil)
|
||||
Reference in New Issue
Block a user