106 lines
2.7 KiB
Go
106 lines
2.7 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|