a73x

internal/server/hub/hub.go

Ref:   Size: 1.5 KiB   History

// Package hub wakes per-host QUIC streams when desired state changes.
package hub

import "sync"

type Hub struct {
	mu sync.Mutex
	m  map[string]chan struct{}
}

func New() *Hub { return &Hub{m: map[string]chan struct{}{}} }

// Subscribe returns a channel that receives a poke whenever desired state
// changes for hostID, and a cancel function to deregister. A second Subscribe
// for the same host supersedes the first (last-writer-wins for reconnecting
// agents); the displaced channel is closed so any orphaned reader unblocks.
// The returned channel is closed when the subscription is cancelled or superseded,
// so callers using "for range" on the channel will exit naturally.
func (h *Hub) Subscribe(hostID string) (<-chan struct{}, func()) {
	h.mu.Lock()
	defer h.mu.Unlock()
	ch := make(chan struct{}, 1)
	if old := h.m[hostID]; old != nil {
		close(old)
	}
	h.m[hostID] = ch
	return ch, func() {
		h.mu.Lock()
		defer h.mu.Unlock()
		if h.m[hostID] == ch {
			delete(h.m, hostID)
			close(ch) // unblocks any goroutine ranging over this channel
		}
	}
}

func (h *Hub) Poke(hostID string) {
	// The send happens under the lock: Subscribe closes displaced channels,
	// and a send racing that close would panic. The send is non-blocking
	// (buffered-1 + default), so holding the lock here cannot deadlock.
	h.mu.Lock()
	defer h.mu.Unlock()
	ch := h.m[hostID]
	if ch == nil {
		return
	}
	select {
	case ch <- struct{}{}:
	default: // already pending; level-triggered consumers re-read full state anyway
	}
}