feat(sse): per-connection filtering with user and workstation context

- Extend sseClient with userID, workstationID, and item filter set
- Update Subscribe() to accept userID and workstationID params
- Add WatchItem/UnwatchItem/IsWatchingItem methods on sseClient
- Add PublishToItem, PublishToWorkstation, PublishToUser targeted delivery
- Targeted events get IDs but skip history ring buffer (real-time only)
- Update HandleEvents to pass auth user ID and workstation_id query param
- Touch workstation last_seen on SSE connect
- Existing Publish() broadcast unchanged; all current callers unaffected
- Add 5 new tests for targeted delivery and item watch lifecycle

Closes #162
This commit is contained in:
Forbes
2026-03-01 10:04:01 -06:00
parent e5cae28a8c
commit e7da3ee94d
4 changed files with 223 additions and 14 deletions

View File

@@ -5,6 +5,8 @@ import (
"net/http"
"strconv"
"time"
"github.com/kindredsystems/silo/internal/auth"
)
// HandleEvents serves the SSE event stream.
@@ -31,9 +33,19 @@ func (s *Server) HandleEvents(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no") // nginx: disable proxy buffering
client := s.broker.Subscribe()
userID := ""
if user := auth.UserFromContext(r.Context()); user != nil {
userID = user.ID
}
wsID := r.URL.Query().Get("workstation_id")
client := s.broker.Subscribe(userID, wsID)
defer s.broker.Unsubscribe(client)
if wsID != "" {
s.workstations.Touch(r.Context(), wsID)
}
// Replay missed events if Last-Event-ID is present.
if lastIDStr := r.Header.Get("Last-Event-ID"); lastIDStr != "" {
if lastID, err := strconv.ParseUint(lastIDStr, 10, 64); err == nil {