package httpapi import ( "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" "data-terminal-cloud/internal/identity" "data-terminal-cloud/internal/webevents" ) func StreamWebEventsHandler(service *webevents.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := identity.PrincipalFromContext(r.Context()) if !ok { WriteError(w, r, http.StatusUnauthorized, "AUTHENTICATION_REQUIRED", "请先登录后再访问") return } lastIDText := strings.TrimSpace(r.Header.Get("Last-Event-ID")) var cursor int64 var err error if lastIDText == "" { cursor, err = service.StartingCursor(r.Context(), principal) } else { cursor, err = strconv.ParseInt(lastIDText, 10, 64) if err == nil && cursor < 1 { err = errors.New("cursor must be positive") } if err == nil { err = service.ValidateCursor(r.Context(), principal, cursor) } } if err != nil { status := http.StatusBadRequest code := "EVENT_CURSOR_INVALID" message := "状态事件编号无效" if errors.Is(err, webevents.ErrCursorUnavailable) { status = http.StatusGone code = "EVENT_CURSOR_EXPIRED" message = "状态事件续传窗口已过期,请重新载入页面" } WriteError(w, r, status, code, message) return } w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") controller := http.NewResponseController(w) _ = controller.SetWriteDeadline(time.Time{}) if _, err := fmt.Fprintf(w, "retry: 2000\nid: %d\n\n", cursor); err != nil { return } if err := controller.Flush(); err != nil { return } poll := time.NewTicker(500 * time.Millisecond) heartbeat := time.NewTicker(10 * time.Second) end := time.NewTimer(25 * time.Second) defer poll.Stop() defer heartbeat.Stop() defer end.Stop() for { select { case <-r.Context().Done(): return case <-end.C: return case <-heartbeat.C: if _, err := fmt.Fprint(w, ": heartbeat\n\n"); err != nil { return } if err := controller.Flush(); err != nil { return } case <-poll.C: items, err := service.ListAfter(r.Context(), principal, cursor, 100) if err != nil { return } for _, item := range items { payload, err := json.Marshal(item) if err != nil { return } if _, err := fmt.Fprintf(w, "id: %d\nevent: %s\ndata: %s\n\n", item.ID, item.Type, payload); err != nil { return } cursor = item.ID } if len(items) > 0 { if err := controller.Flush(); err != nil { return } } } } }) }