0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-16 15:23:53 +02:00
gitea/routers/web/websocket/websocket.go
Epid 256aeb9dc9 feat(websocket): Phase 2 — migrate stopwatches/logout to WebSocket, remove SSE
- Add stopwatch_notifier.go: periodic poller publishes stopwatches via pubsub broker
- Add logout_publisher.go: PublishLogout publishes logout events via pubsub broker
- websocket.go: rewrite logout messages server-side (sessionID to here/elsewhere)
- Remove entire SSE infrastructure: eventsource module, /user/events route, events.go
- Update blockexpensive/qos to handle /-/ws instead of /user/events
- Simplify eventsource.sharedworker.ts: remove EventSource, WebSocket-only delivery
2026-04-02 00:44:20 +03:00

85 lines
2.2 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package websocket
import (
"net/http"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/pubsub"
gitea_ws "github.com/coder/websocket"
)
// logoutBrokerMsg is the internal broker message published by PublishLogout.
type logoutBrokerMsg struct {
Type string `json:"type"`
SessionID string `json:"sessionID,omitempty"`
}
// logoutClientMsg is sent to the WebSocket client so the browser can tell
// whether the logout originated from this tab ("here") or another ("elsewhere").
type logoutClientMsg struct {
Type string `json:"type"`
Data string `json:"data"`
}
// rewriteLogout intercepts a broker logout message and rewrites it to the
// client format using "here"/"elsewhere" instead of the raw session ID.
// If sessionID is empty the logout applies to all sessions ("here" for all).
func rewriteLogout(msg []byte, connSessionID string) []byte {
var lm logoutBrokerMsg
if err := json.Unmarshal(msg, &lm); err != nil || lm.Type != "logout" {
return msg
}
where := "elsewhere"
if lm.SessionID == "" || lm.SessionID == connSessionID {
where = "here"
}
out, err := json.Marshal(logoutClientMsg{Type: "logout", Data: where})
if err != nil {
return msg
}
return out
}
// Serve handles WebSocket upgrade and event delivery for the signed-in user.
func Serve(ctx *context.Context) {
if !ctx.IsSigned {
ctx.Status(http.StatusUnauthorized)
return
}
conn, err := gitea_ws.Accept(ctx.Resp, ctx.Req, &gitea_ws.AcceptOptions{
InsecureSkipVerify: false,
})
if err != nil {
log.Error("websocket: accept failed: %v", err)
return
}
defer conn.CloseNow() //nolint:errcheck // CloseNow is best-effort; error is intentionally ignored
sessionID := ctx.Session.ID()
ch, cancel := pubsub.DefaultBroker.Subscribe(pubsub.UserTopic(ctx.Doer.ID))
defer cancel()
wsCtx := ctx.Req.Context()
for {
select {
case <-wsCtx.Done():
return
case msg, ok := <-ch:
if !ok {
return
}
msg = rewriteLogout(msg, sessionID)
if err := conn.Write(wsCtx, gitea_ws.MessageText, msg); err != nil {
log.Trace("websocket: write failed: %v", err)
return
}
}
}
}