101 lines
2.7 KiB
Go
101 lines
2.7 KiB
Go
// Package gapactivationinmem provides an in-memory
|
|
// ports.GapActivationStore implementation for service-level tests. The
|
|
// stub records every MarkActivated call and offers WasActivated /
|
|
// ActivatedAt accessors so test bodies can assert the gap-window trigger
|
|
// fired exactly once.
|
|
package gapactivationinmem
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"galaxy/lobby/internal/domain/common"
|
|
"galaxy/lobby/internal/ports"
|
|
)
|
|
|
|
// Store is a concurrency-safe in-memory implementation of
|
|
// ports.GapActivationStore. The zero value is not usable; call NewStore
|
|
// to construct.
|
|
type Store struct {
|
|
mu sync.Mutex
|
|
records map[common.GameID]time.Time
|
|
}
|
|
|
|
// NewStore constructs one empty Store ready for use.
|
|
func NewStore() *Store {
|
|
return &Store{records: make(map[common.GameID]time.Time)}
|
|
}
|
|
|
|
// MarkActivated mirrors ports.GapActivationStore semantics: SETNX —
|
|
// the first call wins, subsequent calls are silent no-ops.
|
|
func (store *Store) MarkActivated(ctx context.Context, gameID common.GameID, at time.Time) error {
|
|
if store == 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")
|
|
}
|
|
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
if _, exists := store.records[gameID]; exists {
|
|
return nil
|
|
}
|
|
store.records[gameID] = at.UTC()
|
|
return nil
|
|
}
|
|
|
|
// Get reports the activation time previously written for gameID.
|
|
func (store *Store) Get(ctx context.Context, gameID common.GameID) (time.Time, bool, error) {
|
|
if store == 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)
|
|
}
|
|
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
at, ok := store.records[gameID]
|
|
return at, ok, nil
|
|
}
|
|
|
|
// WasActivated reports whether MarkActivated has been called for gameID.
|
|
func (store *Store) WasActivated(gameID common.GameID) bool {
|
|
if store == nil {
|
|
return false
|
|
}
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
_, ok := store.records[gameID]
|
|
return ok
|
|
}
|
|
|
|
// ActivatedAt returns the recorded activation time for gameID, or zero
|
|
// time when the game has not been activated.
|
|
func (store *Store) ActivatedAt(gameID common.GameID) time.Time {
|
|
if store == nil {
|
|
return time.Time{}
|
|
}
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
return store.records[gameID]
|
|
}
|
|
|
|
// Compile-time interface assertion.
|
|
var _ ports.GapActivationStore = (*Store)(nil)
|