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) }