Files
Gitea/services/websocket/websocket.go
T
silverwind 7308c5fb97 enhance(websocket): improve client synchronization (#38847)
Keep notification counts and stopwatches in sync when tabs open or
reconnect, closing the gap behind [this
flake](https://github.com/go-gitea/gitea/actions/runs/31312525323/job/93242429394).

1. Send current notification counts and stopwatch state on each
WebSocket connection, replacing the separate state fetches tabs made on
every connect.
2. Tabs joining an already open connection fetch current state once,
since its snapshot went out before they attached.
3. Send events queued during the handshake before the snapshot, so older
events can't roll it back.
4. Wait for the Redis SUBSCRIBE acknowledgement, so the snapshot read
after subscribing can't miss an event.
2026-10-29 05:18:17 +00:00

72 lines
1.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package websocket
import (
"bytes"
"context"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
"gitea.dev/modules/setting"
notify_service "gitea.dev/services/notify"
"gitea.dev/services/pubsub"
)
type wsNotifier struct {
notify_service.NullNotifier
}
var _ notify_service.Notifier = &wsNotifier{}
func Init() error {
// the pubsub broker must be ready before the notifier starts publishing to it
if err := pubsub.Init(); err != nil {
return err
}
notify_service.RegisterNotifier(&wsNotifier{})
return nil
}
func UserData(ctx context.Context, user *user_model.User) (data [][]byte) {
for _, event := range [][]byte{notificationCountEvent(ctx, user.ID), stopwatchesEvent(ctx, user)} {
if event != nil {
data = append(data, event)
}
}
return data
}
func EventForSession(brokerPayload []byte, sessionID string) []byte {
target, eventData, _ := bytes.Cut(brokerPayload, []byte("\n"))
if len(target) > 0 && string(target) != sessionID {
return nil
}
return eventData
}
// makeEventMessage prefixes the client JSON with its target session, empty for all sessions.
func makeEventMessage(eventType, targetSession string, eventData any) []byte {
buf := bytes.NewBufferString(targetSession + "\n")
err := json.MarshalWriteRelaxed(buf, &struct {
EventType string `json:"eventType"`
EventData any `json:"eventData,omitzero"`
}{eventType, eventData})
if err != nil {
setting.PanicInDevOrTesting("websocket: marshal event: %v", err)
return nil
}
return buf.Bytes()
}
func publishUserEvent(userID int64, build func() []byte) {
topic := pubsub.UserTopic(userID)
if !pubsub.DefaultBroker.HasTopicSubscribers(topic) {
return
}
if event := build(); event != nil {
pubsub.DefaultBroker.Publish(topic, event)
}
}