A per-DataDir background process owns ticket JSON, history, and sync so multiple TUI instances stay consistent; MCP token APIs are ready for phase 2. Co-authored-by: Cursor <cursoragent@cursor.com>
82 lines
1.4 KiB
Go
82 lines
1.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
)
|
|
|
|
// Event is a change notification pushed to SSE clients.
|
|
type Event struct {
|
|
Type string `json:"type"`
|
|
ProjectID string `json:"projectId,omitempty"`
|
|
}
|
|
|
|
type hub struct {
|
|
mu sync.Mutex
|
|
clients map[chan Event]struct{}
|
|
}
|
|
|
|
func newHub() *hub {
|
|
return &hub{clients: map[chan Event]struct{}{}}
|
|
}
|
|
|
|
func (h *hub) subscribe() chan Event {
|
|
ch := make(chan Event, 16)
|
|
h.mu.Lock()
|
|
h.clients[ch] = struct{}{}
|
|
h.mu.Unlock()
|
|
return ch
|
|
}
|
|
|
|
func (h *hub) unsubscribe(ch chan Event) {
|
|
h.mu.Lock()
|
|
delete(h.clients, ch)
|
|
h.mu.Unlock()
|
|
close(ch)
|
|
}
|
|
|
|
func (h *hub) publish(ev Event) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
for ch := range h.clients {
|
|
select {
|
|
case ch <- ev:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
|
|
ch := s.hub.subscribe()
|
|
defer s.hub.unsubscribe(ch)
|
|
|
|
fmt.Fprintf(w, ": ok\n\n")
|
|
flusher.Flush()
|
|
|
|
ctx := r.Context()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case ev, ok := <-ch:
|
|
if !ok {
|
|
return
|
|
}
|
|
data, _ := json.Marshal(ev)
|
|
fmt.Fprintf(w, "data: %s\n\n", data)
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|