feat: wire sse_client_cpp_core for live updates from /api/boards/issues/stream
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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
|
||||
Reference in New Issue
Block a user