feat: wire sse_client_cpp_core for live updates from /api/boards/issues/stream
This commit is contained in:
@@ -163,6 +163,14 @@ func handlePatchBoardCard() http.HandlerFunc {
|
||||
}
|
||||
_ = PatchFrontmatterField(file, "updated", time.Now().UTC().Format("2006-01-02"))
|
||||
cache.invalidate()
|
||||
if globalHub != nil {
|
||||
globalHub.Broadcast(ServerEvent{
|
||||
Board: board,
|
||||
CardID: id,
|
||||
Action: "updated",
|
||||
EventType: "card_changed",
|
||||
})
|
||||
}
|
||||
infra.HTTPJSONResponse(w, http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"id": id,
|
||||
@@ -243,11 +251,95 @@ func handleLaunchBoardCard() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/boards/{board}/stream (text/event-stream)
|
||||
//
|
||||
// Long-lived SSE connection that emits one event per card change on the
|
||||
// given board. Events:
|
||||
// - card_added {"board","card_id","action":"created"}
|
||||
// - card_changed {"board","card_id","action":"updated"}
|
||||
// - card_removed {"board","card_id","action":"deleted"}
|
||||
// - keepalive ts=<unix>
|
||||
//
|
||||
// Events for OTHER boards are filtered out (one subscription per board).
|
||||
// A keepalive is emitted every 15s to prevent proxy timeouts.
|
||||
func handleBoardStream() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
board := r.PathValue("board")
|
||||
dir, _, _ := dirAndCacheForBoard(board)
|
||||
if dir == "" {
|
||||
notFound(w, "unknown board: "+board)
|
||||
return
|
||||
}
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if globalHub == nil {
|
||||
http.Error(w, "hub not initialised", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher.Flush()
|
||||
|
||||
ch := globalHub.Subscribe()
|
||||
defer globalHub.Unsubscribe(ch)
|
||||
|
||||
// Honor Last-Event-ID is not supported yet (TODO: replay buffer).
|
||||
_ = r.Header.Get("Last-Event-ID")
|
||||
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
ctx := r.Context()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if _, err := fmt.Fprintf(w, "event: keepalive\ndata: ts=%d\n\n", time.Now().Unix()); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
case ev, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ev.Board != board {
|
||||
continue
|
||||
}
|
||||
evType := ev.EventType
|
||||
if evType == "" {
|
||||
evType = "card_changed"
|
||||
}
|
||||
payload, err := json.Marshal(map[string]string{
|
||||
"board": ev.Board,
|
||||
"card_id": ev.CardID,
|
||||
"action": ev.Action,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", evType, payload); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// boardRoutes returns the additional routes for issues/flows boards. Called
|
||||
// from apiRoutes() in handlers.go.
|
||||
func boardRoutes() []infra.Route {
|
||||
return []infra.Route{
|
||||
{Method: "GET", Path: "/api/boards/{board}/cards", Handler: handleListBoardCards()},
|
||||
{Method: "GET", Path: "/api/boards/{board}/stream", Handler: handleBoardStream()},
|
||||
{Method: "PATCH", Path: "/api/boards/{board}/cards/{id}", Handler: handlePatchBoardCard()},
|
||||
{Method: "POST", Path: "/api/boards/{board}/cards/{id}/launch", Handler: handleLaunchBoardCard()},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user