package hub import ( "fmt" "os" "sync" "github.com/zzet/gortex/internal/indexer" ) // Hub fans out watcher events to multiple subscribers. type Hub struct { subscribers map[string]chan indexer.GraphChangeEvent mu sync.RWMutex nextID int done chan struct{} } // New creates a new Hub. func New() *Hub { return &Hub{ subscribers: make(map[string]chan indexer.GraphChangeEvent), done: make(chan struct{}), } } // Run reads from the watcher events channel and broadcasts to all subscribers. // It also logs each event to stderr. Blocks until the events channel is closed // or Stop is called. func (h *Hub) Run(events <-chan indexer.GraphChangeEvent) { for { select { case ev, ok := <-events: if !ok { return } // Log to stderr (replaces the inline goroutine in serve.go) fmt.Fprintf(os.Stderr, "[gortex watch] %-10s %s +%d nodes +%d edges -%d nodes -%d edges (%dms)\n", ev.Kind, ev.FilePath, ev.NodesAdded, ev.EdgesAdded, ev.NodesRemoved, ev.EdgesRemoved, ev.DurationMs) h.broadcast(ev) case <-h.done: return } } } func (h *Hub) broadcast(ev indexer.GraphChangeEvent) { h.mu.RLock() defer h.mu.RUnlock() for _, ch := range h.subscribers { select { case ch <- ev: default: // Slow subscriber — drop event to avoid blocking } } } // Subscribe creates a new subscriber channel. Returns an ID for unsubscribing // and a receive-only channel that will receive graph change events. func (h *Hub) Subscribe() (string, <-chan indexer.GraphChangeEvent) { h.mu.Lock() defer h.mu.Unlock() h.nextID++ id := fmt.Sprintf("sub-%d", h.nextID) ch := make(chan indexer.GraphChangeEvent, 16) h.subscribers[id] = ch return id, ch } // Unsubscribe removes a subscriber and closes its channel. func (h *Hub) Unsubscribe(id string) { h.mu.Lock() defer h.mu.Unlock() if ch, ok := h.subscribers[id]; ok { close(ch) delete(h.subscribers, id) } } // SubscriberCount returns the number of active subscribers. Tests use this // to wait for an SSE client to finish registering before publishing events // that the client needs to observe — broadcast() drops messages silently // when a subscriber hasn't registered yet, so timing races are easy to // write without a way to synchronize. func (h *Hub) SubscriberCount() int { h.mu.RLock() defer h.mu.RUnlock() return len(h.subscribers) } // Stop signals the hub to stop processing events. func (h *Hub) Stop() { select { case <-h.done: default: close(h.done) } }