feat: initial commit
build / test (push) Successful in 9s
build / build (push) Successful in 10s
build / build-image (push) Successful in 1m4s

This commit is contained in:
2026-06-04 14:19:37 +02:00
commit cf961ab94f
28 changed files with 3036 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
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)
}