Files
silo-server/internal/playback/realtime_hub.go
T

145 lines
3.6 KiB
Go

package playback
import (
"errors"
"sync"
)
// ErrRealtimeConnectionNotFound is returned when a session has no active realtime connection.
var ErrRealtimeConnectionNotFound = errors.New("realtime connection not found")
// RealtimeConnection is the minimal send interface required by the hub.
// Implementations must ensure WriteJSON is bounded by an external deadline or
// cancellation policy; the hub serializes writes per session and assumes each
// write returns in finite time.
type RealtimeConnection interface {
WriteJSON(v any) error
}
type sessionLane struct {
conn RealtimeConnection
mu sync.Mutex
closed bool
generation uint64
}
// RealtimeRegistration is an opaque ownership token for a realtime connection.
type RealtimeRegistration struct {
sessionID string
lane *sessionLane
generation uint64
}
// RealtimeHub stores one active realtime connection per playback session.
type RealtimeHub struct {
mu sync.RWMutex
connections map[string]*sessionLane
onInitialRegister func()
onRegisterLaneLookup func(sessionID string, lane *sessionLane)
}
// NewRealtimeHub creates an empty realtime hub.
func NewRealtimeHub() *RealtimeHub {
return &RealtimeHub{
connections: make(map[string]*sessionLane),
}
}
// Register associates a realtime connection with a playback session.
// A later registration for the same session replaces the prior connection.
func (h *RealtimeHub) Register(sessionID string, conn RealtimeConnection) *RealtimeRegistration {
if h == nil || sessionID == "" || conn == nil {
return nil
}
h.mu.Lock()
lane := h.connections[sessionID]
if lane == nil {
lane = &sessionLane{conn: conn, generation: 1}
h.connections[sessionID] = lane
h.mu.Unlock()
if h.onInitialRegister != nil {
h.onInitialRegister()
}
return &RealtimeRegistration{sessionID: sessionID, lane: lane, generation: 1}
}
h.mu.Unlock()
if h.onRegisterLaneLookup != nil {
h.onRegisterLaneLookup(sessionID, lane)
}
lane.mu.Lock()
if lane.closed {
lane.mu.Unlock()
return nil
}
lane.conn = conn
lane.closed = false
lane.generation++
reg := &RealtimeRegistration{
sessionID: sessionID,
lane: lane,
generation: lane.generation,
}
lane.mu.Unlock()
return reg
}
// Unregister removes the active realtime connection for the given registration
// token only if it still matches the currently registered connection.
func (h *RealtimeHub) Unregister(reg *RealtimeRegistration) bool {
if h == nil || reg == nil || reg.sessionID == "" || reg.lane == nil {
return false
}
h.mu.RLock()
lane, ok := h.connections[reg.sessionID]
h.mu.RUnlock()
if !ok || lane == nil || lane != reg.lane {
return false
}
lane.mu.Lock()
if lane.generation != reg.generation || lane.closed {
lane.mu.Unlock()
return false
}
lane.closed = true
lane.conn = nil
lane.generation++
nextGeneration := lane.generation
h.mu.Lock()
if current, ok := h.connections[reg.sessionID]; ok && current == lane && lane.closed && lane.conn == nil && lane.generation == nextGeneration {
delete(h.connections, reg.sessionID)
}
h.mu.Unlock()
lane.mu.Unlock()
return true
}
// Send writes a message to the active connection for the given session.
func (h *RealtimeHub) Send(sessionID string, message any) error {
if h == nil || sessionID == "" {
return ErrRealtimeConnectionNotFound
}
h.mu.RLock()
lane, ok := h.connections[sessionID]
if !ok || lane == nil {
h.mu.RUnlock()
return ErrRealtimeConnectionNotFound
}
h.mu.RUnlock()
lane.mu.Lock()
if lane.closed || lane.conn == nil {
lane.mu.Unlock()
return ErrRealtimeConnectionNotFound
}
err := lane.conn.WriteJSON(message)
lane.mu.Unlock()
return err
}