feat: runtime manager
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// 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)
|
||||
Reference in New Issue
Block a user