Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

101 lines
2.5 KiB
Go

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)
}
}