- ClockEngine with full state machine (stopped/running/paused transitions) - Level management: load, advance, rewind, jump, hand-for-hand mode - Drift-free ticker at 100ms with 1/sec broadcast (10/sec in final 10s) - ClockRegistry for multi-tournament support (thread-safe) - ClockSnapshot for reconnecting clients (CLOCK-09) - Configurable overtime mode (repeat/stop) - Crash recovery via RestoreState (resumes as paused for safety) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
2.6 KiB
Go
116 lines
2.6 KiB
Go
package clock
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/felt-app/felt/internal/server/ws"
|
|
)
|
|
|
|
// Registry manages multiple clock engines, one per tournament.
|
|
// Thread-safe for concurrent access.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
engines map[string]*ClockEngine
|
|
cancels map[string]context.CancelFunc
|
|
hub *ws.Hub
|
|
}
|
|
|
|
// NewRegistry creates a new clock registry.
|
|
func NewRegistry(hub *ws.Hub) *Registry {
|
|
return &Registry{
|
|
engines: make(map[string]*ClockEngine),
|
|
cancels: make(map[string]context.CancelFunc),
|
|
hub: hub,
|
|
}
|
|
}
|
|
|
|
// GetOrCreate returns the clock engine for the given tournament,
|
|
// creating one if it doesn't exist.
|
|
func (r *Registry) GetOrCreate(tournamentID string) *ClockEngine {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if engine, ok := r.engines[tournamentID]; ok {
|
|
return engine
|
|
}
|
|
|
|
engine := NewClockEngine(tournamentID, r.hub)
|
|
r.engines[tournamentID] = engine
|
|
return engine
|
|
}
|
|
|
|
// Get returns the clock engine for the given tournament, or nil if not found.
|
|
func (r *Registry) Get(tournamentID string) *ClockEngine {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return r.engines[tournamentID]
|
|
}
|
|
|
|
// StartTicker starts the ticker goroutine for the given tournament's engine.
|
|
// If a ticker is already running, it is stopped first.
|
|
func (r *Registry) StartTicker(ctx context.Context, tournamentID string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
engine, ok := r.engines[tournamentID]
|
|
if !ok {
|
|
return fmt.Errorf("clock: no engine for tournament %s", tournamentID)
|
|
}
|
|
|
|
// Stop existing ticker if any
|
|
if cancel, ok := r.cancels[tournamentID]; ok {
|
|
cancel()
|
|
}
|
|
|
|
tickerCtx, cancel := context.WithCancel(ctx)
|
|
r.cancels[tournamentID] = cancel
|
|
|
|
go StartTicker(tickerCtx, engine)
|
|
return nil
|
|
}
|
|
|
|
// StopTicker stops the ticker for the given tournament.
|
|
func (r *Registry) StopTicker(tournamentID string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if cancel, ok := r.cancels[tournamentID]; ok {
|
|
cancel()
|
|
delete(r.cancels, tournamentID)
|
|
}
|
|
}
|
|
|
|
// Remove removes a clock engine and stops its ticker.
|
|
func (r *Registry) Remove(tournamentID string) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if cancel, ok := r.cancels[tournamentID]; ok {
|
|
cancel()
|
|
delete(r.cancels, tournamentID)
|
|
}
|
|
delete(r.engines, tournamentID)
|
|
}
|
|
|
|
// Count returns the number of active clock engines.
|
|
func (r *Registry) Count() int {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return len(r.engines)
|
|
}
|
|
|
|
// Shutdown stops all tickers and clears all engines.
|
|
func (r *Registry) Shutdown() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
for id, cancel := range r.cancels {
|
|
cancel()
|
|
delete(r.cancels, id)
|
|
}
|
|
for id := range r.engines {
|
|
delete(r.engines, id)
|
|
}
|
|
}
|