168 lines
3.1 KiB
Go
168 lines
3.1 KiB
Go
package playback
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// CommandState describes the lifecycle state of a tracked realtime command.
|
|
type CommandState string
|
|
|
|
const (
|
|
CommandStatePending CommandState = "pending"
|
|
CommandStateAccepted CommandState = "accepted"
|
|
CommandStateCompleted CommandState = "completed"
|
|
)
|
|
|
|
type commandTrackerEntry struct {
|
|
state CommandState
|
|
timer *time.Timer
|
|
fallback func()
|
|
}
|
|
|
|
// CommandTracker tracks in-flight realtime commands by command_id.
|
|
type CommandTracker struct {
|
|
mu sync.Mutex
|
|
closed bool
|
|
commands map[string]*commandTrackerEntry
|
|
}
|
|
|
|
// NewCommandTracker creates an empty command tracker.
|
|
func NewCommandTracker() *CommandTracker {
|
|
return &CommandTracker{
|
|
commands: make(map[string]*commandTrackerEntry),
|
|
}
|
|
}
|
|
|
|
// Track records a new command as pending and arms its fallback deadline.
|
|
func (t *CommandTracker) Track(commandID string, deadline time.Duration, fallback func()) {
|
|
if t == nil || commandID == "" {
|
|
return
|
|
}
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.closed {
|
|
return
|
|
}
|
|
if _, exists := t.commands[commandID]; exists {
|
|
return
|
|
}
|
|
|
|
entry := &commandTrackerEntry{
|
|
state: CommandStatePending,
|
|
fallback: fallback,
|
|
}
|
|
t.commands[commandID] = entry
|
|
entry.timer = time.AfterFunc(deadline, func() {
|
|
t.fireDeadline(commandID)
|
|
})
|
|
}
|
|
|
|
// Ack marks a tracked command as accepted.
|
|
func (t *CommandTracker) Ack(commandID string) {
|
|
t.setState(commandID, CommandStateAccepted)
|
|
}
|
|
|
|
// Result marks a tracked command as completed.
|
|
func (t *CommandTracker) Result(commandID string) {
|
|
t.mu.Lock()
|
|
if t.closed {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
entry, ok := t.commands[commandID]
|
|
if !ok {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
entry.state = CommandStateCompleted
|
|
timer := entry.timer
|
|
delete(t.commands, commandID)
|
|
t.mu.Unlock()
|
|
|
|
if timer != nil {
|
|
timer.Stop()
|
|
}
|
|
}
|
|
|
|
// Status reports the current tracked state for a command.
|
|
func (t *CommandTracker) Status(commandID string) (CommandState, bool) {
|
|
if t == nil || commandID == "" {
|
|
return "", false
|
|
}
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
entry, ok := t.commands[commandID]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
return entry.state, true
|
|
}
|
|
|
|
// Close stops all deadline timers and prevents future tracking.
|
|
func (t *CommandTracker) Close() {
|
|
if t == nil {
|
|
return
|
|
}
|
|
|
|
t.mu.Lock()
|
|
if t.closed {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
t.closed = true
|
|
commands := t.commands
|
|
t.commands = make(map[string]*commandTrackerEntry)
|
|
t.mu.Unlock()
|
|
|
|
for _, entry := range commands {
|
|
if entry.timer != nil {
|
|
entry.timer.Stop()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *CommandTracker) setState(commandID string, state CommandState) {
|
|
t.mu.Lock()
|
|
if t.closed {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
entry, ok := t.commands[commandID]
|
|
if !ok {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
if entry.state != CommandStateCompleted {
|
|
entry.state = state
|
|
}
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *CommandTracker) fireDeadline(commandID string) {
|
|
t.mu.Lock()
|
|
if t.closed {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
entry, ok := t.commands[commandID]
|
|
if !ok {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
if entry.state == CommandStateCompleted {
|
|
delete(t.commands, commandID)
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
fallback := entry.fallback
|
|
delete(t.commands, commandID)
|
|
t.mu.Unlock()
|
|
|
|
if fallback != nil {
|
|
fallback()
|
|
}
|
|
}
|