113 lines
2.6 KiB
Go
113 lines
2.6 KiB
Go
package scoreboard
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Snapshot is an immutable view of the scoreboard at a point in time.
|
|
type Snapshot struct {
|
|
Entries []Entry
|
|
FetchedAt time.Time
|
|
byRank map[int]Entry
|
|
byUser map[string]Entry // lower-cased username -> entry
|
|
}
|
|
|
|
func newSnapshot(entries []Entry, at time.Time) *Snapshot {
|
|
s := &Snapshot{
|
|
Entries: entries,
|
|
FetchedAt: at,
|
|
byRank: make(map[int]Entry, len(entries)),
|
|
byUser: make(map[string]Entry, len(entries)),
|
|
}
|
|
for _, e := range entries {
|
|
s.byRank[e.Rank] = e
|
|
s.byUser[strings.ToLower(e.Username)] = e
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Store holds the latest scoreboard snapshot and serves concurrent reads.
|
|
// It keeps the last good snapshot when a refresh fails.
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
snap *Snapshot
|
|
}
|
|
|
|
// NewStore returns an empty store. Ready reports false until Set is called.
|
|
func NewStore() *Store { return &Store{} }
|
|
|
|
// Set atomically replaces the current snapshot with the given entries.
|
|
func (s *Store) Set(entries []Entry, at time.Time) {
|
|
snap := newSnapshot(entries, at)
|
|
s.mu.Lock()
|
|
s.snap = snap
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
// Ready reports whether a snapshot has been loaded.
|
|
func (s *Store) Ready() bool {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.snap != nil
|
|
}
|
|
|
|
// Snapshot returns the current snapshot (nil if none loaded yet).
|
|
func (s *Store) Snapshot() *Snapshot {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.snap
|
|
}
|
|
|
|
// List returns a page of entries ordered by rank, honoring offset and limit.
|
|
// limit <= 0 means "all from offset". It also returns the snapshot time.
|
|
func (s *Store) List(offset, limit int) ([]Entry, time.Time) {
|
|
snap := s.Snapshot()
|
|
if snap == nil {
|
|
return nil, time.Time{}
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if offset >= len(snap.Entries) {
|
|
return []Entry{}, snap.FetchedAt
|
|
}
|
|
end := len(snap.Entries)
|
|
if limit > 0 && offset+limit < end {
|
|
end = offset + limit
|
|
}
|
|
out := make([]Entry, end-offset)
|
|
copy(out, snap.Entries[offset:end])
|
|
return out, snap.FetchedAt
|
|
}
|
|
|
|
// ByRank returns the entry at the given canonical rank.
|
|
func (s *Store) ByRank(rank int) (Entry, bool) {
|
|
snap := s.Snapshot()
|
|
if snap == nil {
|
|
return Entry{}, false
|
|
}
|
|
e, ok := snap.byRank[rank]
|
|
return e, ok
|
|
}
|
|
|
|
// ByUsername returns the entry for the given username (case-insensitive).
|
|
func (s *Store) ByUsername(username string) (Entry, bool) {
|
|
snap := s.Snapshot()
|
|
if snap == nil {
|
|
return Entry{}, false
|
|
}
|
|
e, ok := snap.byUser[strings.ToLower(strings.TrimSpace(username))]
|
|
return e, ok
|
|
}
|
|
|
|
// Len returns the number of entries in the current snapshot.
|
|
func (s *Store) Len() int {
|
|
snap := s.Snapshot()
|
|
if snap == nil {
|
|
return 0
|
|
}
|
|
return len(snap.Entries)
|
|
}
|