package main import ( "sync" ) // ServerEvent is a board-scoped event broadcast to all SSE subscribers // of a given board. It is emitted both by the fsnotify watcher (file // changes on disk under dev/issues or dev/flows) and by handlers that // mutate cards (PATCH /api/boards/{board}/cards/{id}) so the C++ client // updates in real time without polling. type ServerEvent struct { Board string `json:"board"` // "issues" | "flows" CardID string `json:"card_id"` // canonical id, e.g. "0119" Action string `json:"action"` // "created" | "updated" | "deleted" EventType string `json:"-"` // SSE "event:" field. Empty → "card_changed" } // Hub fans out ServerEvent messages to N concurrent subscribers. Each // subscriber gets a buffered channel; if the channel is full, the event // is dropped for THAT subscriber (slow consumer must reconnect to get a // fresh snapshot). Hub itself is safe for concurrent use. type Hub struct { mu sync.RWMutex clients map[chan ServerEvent]struct{} } // NewHub returns an empty Hub ready to use. func NewHub() *Hub { return &Hub{ clients: make(map[chan ServerEvent]struct{}), } } // Subscribe registers a new subscriber. The returned channel is buffered // (16) so a brief stall on the consumer side doesn't block the producer. func (h *Hub) Subscribe() chan ServerEvent { ch := make(chan ServerEvent, 16) h.mu.Lock() h.clients[ch] = struct{}{} h.mu.Unlock() return ch } // Unsubscribe removes a subscriber and closes its channel. Idempotent. func (h *Hub) Unsubscribe(ch chan ServerEvent) { h.mu.Lock() defer h.mu.Unlock() if _, ok := h.clients[ch]; !ok { return } delete(h.clients, ch) close(ch) } // Broadcast sends ev to every current subscriber. Non-blocking: if a // subscriber's channel is full the event is dropped for that subscriber. func (h *Hub) Broadcast(ev ServerEvent) { h.mu.RLock() defer h.mu.RUnlock() for ch := range h.clients { select { case ch <- ev: default: // slow consumer: drop } } } // Count returns the current number of subscribers (test/diagnostic). func (h *Hub) Count() int { h.mu.RLock() defer h.mu.RUnlock() return len(h.clients) } // globalHub is initialised in main() and consumed by handlers + watcher. var globalHub *Hub