🎉 live server seems to be working now
This commit is contained in:
163
third_party/sendspin-go/pkg/sendspin/group_role.go
vendored
Normal file
163
third_party/sendspin-go/pkg/sendspin/group_role.go
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
// ABOUTME: GroupRole interface and role dispatch on Group
|
||||
// ABOUTME: Roles register with the Group and receive client events + messages
|
||||
package sendspin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GroupRole is the interface implemented by per-role handlers. Each
|
||||
// role family (controller, metadata, player, artwork) registers one
|
||||
// GroupRole with the Group. The Group dispatches client lifecycle
|
||||
// events to all registered roles.
|
||||
type GroupRole interface {
|
||||
Role() string
|
||||
OnClientJoin(c *ServerClient)
|
||||
OnClientLeave(id string, name string)
|
||||
}
|
||||
|
||||
// MessageHandler is an optional interface that a GroupRole may
|
||||
// implement to receive role-specific messages (e.g., client/command
|
||||
// payloads routed by role key). Roles that don't handle messages
|
||||
// don't need to implement this.
|
||||
type MessageHandler interface {
|
||||
HandleMessage(c *ServerClient, payload json.RawMessage) error
|
||||
}
|
||||
|
||||
// PlaybackStateChangedHandler is an optional interface that a GroupRole
|
||||
// may implement to receive notifications when the group's playback state
|
||||
// transitions. The Group dispatches GroupPlaybackStateChangedEvent to
|
||||
// roles that implement this interface; same-state writes are not
|
||||
// dispatched (Group.SetPlaybackState filters them out).
|
||||
//
|
||||
// Note: oldState may be empty for the very first transition out of the
|
||||
// zero value.
|
||||
type PlaybackStateChangedHandler interface {
|
||||
OnPlaybackStateChanged(oldState, newState string)
|
||||
}
|
||||
|
||||
// RegisterRole registers a GroupRole with this group. The role is
|
||||
// stored by its Role() family name. Duplicate registrations for the
|
||||
// same family overwrite the previous one.
|
||||
//
|
||||
// After registration, the role receives OnClientJoin / OnClientLeave
|
||||
// calls for clients that join or leave the group, dispatched from
|
||||
// the group's internal event subscriber.
|
||||
func (g *Group) RegisterRole(role GroupRole) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if g.roles == nil {
|
||||
g.roles = make(map[string]GroupRole)
|
||||
}
|
||||
g.roles[role.Role()] = role
|
||||
|
||||
// Optional attach-to-group hook: roles that need to iterate the
|
||||
// group's clients on broadcast (e.g. MetadataGroupRole) implement
|
||||
// the unexported attachToGroup method to receive a back-reference.
|
||||
// Kept unexported so callers can't bypass RegisterRole.
|
||||
if a, ok := role.(interface{ attachToGroup(*Group) }); ok {
|
||||
a.attachToGroup(g)
|
||||
}
|
||||
|
||||
if g.roleDispatchStarted {
|
||||
return
|
||||
}
|
||||
g.roleDispatchStarted = true
|
||||
|
||||
events, _ := g.subscribeInternal()
|
||||
go g.dispatchRoleEvents(events)
|
||||
}
|
||||
|
||||
// GetRole returns the registered GroupRole for the given family name,
|
||||
// or nil if none is registered.
|
||||
func (g *Group) GetRole(family string) GroupRole {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
if g.roles == nil {
|
||||
return nil
|
||||
}
|
||||
return g.roles[family]
|
||||
}
|
||||
|
||||
// RouteMessage routes a role-specific message payload to the
|
||||
// registered GroupRole for the given family. Returns an error if no
|
||||
// role is registered for the family or if the role doesn't implement
|
||||
// MessageHandler.
|
||||
func (g *Group) RouteMessage(c *ServerClient, roleFamily string, payload json.RawMessage) error {
|
||||
g.mu.RLock()
|
||||
role, ok := g.roles[roleFamily]
|
||||
g.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("no role registered for %q", roleFamily)
|
||||
}
|
||||
|
||||
handler, ok := role.(MessageHandler)
|
||||
if !ok {
|
||||
return fmt.Errorf("role %q does not handle messages", roleFamily)
|
||||
}
|
||||
|
||||
return handler.HandleMessage(c, payload)
|
||||
}
|
||||
|
||||
// dispatchRoleEvents reads from the group's event bus and calls
|
||||
// the appropriate lifecycle method on every registered role.
|
||||
func (g *Group) dispatchRoleEvents(events <-chan Event) {
|
||||
for evt := range events {
|
||||
g.mu.RLock()
|
||||
roles := make([]GroupRole, 0, len(g.roles))
|
||||
for _, r := range g.roles {
|
||||
roles = append(roles, r)
|
||||
}
|
||||
g.mu.RUnlock()
|
||||
|
||||
switch e := evt.(type) {
|
||||
case ClientJoinedEvent:
|
||||
for _, r := range roles {
|
||||
r.OnClientJoin(e.Client)
|
||||
}
|
||||
case ClientLeftEvent:
|
||||
for _, r := range roles {
|
||||
r.OnClientLeave(e.ClientID, e.ClientName)
|
||||
}
|
||||
case GroupPlaybackStateChangedEvent:
|
||||
for _, r := range roles {
|
||||
if h, ok := r.(PlaybackStateChangedHandler); ok {
|
||||
h.OnPlaybackStateChanged(e.OldState, e.NewState)
|
||||
}
|
||||
}
|
||||
default:
|
||||
// ClientStateChangedEvent and future event types are
|
||||
// dispatched to roles that subscribe to them via other
|
||||
// mechanisms (e.g., the event bus directly). The role
|
||||
// dispatcher only handles lifecycle events.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// subscribeInternal creates a subscription while the caller already
|
||||
// holds g.mu.Lock. This avoids the deadlock that would occur if we
|
||||
// called the public Subscribe (which also takes Lock).
|
||||
func (g *Group) subscribeInternal() (<-chan Event, func()) {
|
||||
if g.closed {
|
||||
ch := make(chan Event)
|
||||
close(ch)
|
||||
return ch, func() {}
|
||||
}
|
||||
|
||||
id := g.nextSub
|
||||
g.nextSub++
|
||||
ch := make(chan Event, 32)
|
||||
g.subs[id] = ch
|
||||
|
||||
return ch, func() {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if existing, ok := g.subs[id]; ok {
|
||||
delete(g.subs, id)
|
||||
close(existing)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user