Files
MHSanaei fc5be5b9e4 feat(web): broadcast delta client stats above a snapshot threshold
Both 5s broadcasters (the local traffic poll and the node traffic sync)
shipped the complete client_traffics table on every cycle while a browser
was connected. At 500k clients that is a 1.7s full-table read plus an
86MB marshal per job per poll — and the hub drops any payload over 10MB
and sends an invalidate the frontend ignores for these message types, so
past ~55k clients all of it was pure waste and the UI got nothing.

Installs at or below 5000 clients (clientStatsSnapshotMaxClients) keep
the exact full-snapshot behavior — it exists because a pure delta feed
left UI rows stale when nothing moved in a cycle (see GetAllClientTraffics)
— and the payload now carries snapshot=true. Above the threshold the jobs
send only this cycle's active rows (the xray poll's active emails, or the
emails online on the synced nodes) with snapshot=false, and scope the
last-online map to those rows; the initial full map still arrives over
REST and the clients page refetches every 5s.

GetActiveClientTraffics gains the overlayGlobalTraffic pass so delta rows
carry the same cross-panel usage as snapshot rows. The node job also
stops reading the full last-online map before the has-clients gate, which
was a wasted full-table read on every tick with no dashboard open.

Frontend: useClients keeps its live summary strictly snapshot-driven
(snapshot=false payloads skip the allClientStats replace and the summary
falls back to the server-computed one); the per-row page merge and the
inbounds-page merges already handle deltas.
2026-07-02 16:34:01 +02:00

116 lines
3.3 KiB
Go

// Package websocket provides WebSocket hub for real-time updates and notifications.
package websocket
import (
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/web/global"
)
// GetHub returns the global WebSocket hub instance.
func GetHub() *Hub {
webServer := global.GetWebServer()
if webServer == nil {
return nil
}
hub := webServer.GetWSHub()
if hub == nil {
return nil
}
wsHub, ok := hub.(*Hub)
if !ok {
logger.Warning("WebSocket hub type assertion failed")
return nil
}
return wsHub
}
// HasClients returns true if any WebSocket client is connected.
// Use this to skip expensive work (DB queries, serialization) when no browser is open.
func HasClients() bool {
hub := GetHub()
return hub != nil && hub.GetClientCount() > 0
}
// BroadcastStatus broadcasts server status update to all connected clients.
func BroadcastStatus(status any) {
if hub := GetHub(); hub != nil {
hub.Broadcast(MessageTypeStatus, status)
}
}
// BroadcastTraffic broadcasts traffic statistics update to all connected clients.
func BroadcastTraffic(traffic any) {
if hub := GetHub(); hub != nil {
hub.Broadcast(MessageTypeTraffic, traffic)
}
}
// BroadcastClientStats broadcasts absolute per-client traffic counters. Small
// installs send the complete row set each cycle (payload key snapshot=true);
// above the traffic job's snapshot threshold only the rows active in the
// latest collection window are sent (snapshot=false), which keeps the payload
// under the hub's cap at any client count.
func BroadcastClientStats(stats any) {
if hub := GetHub(); hub != nil {
hub.Broadcast(MessageTypeClientStats, stats)
}
}
// BroadcastInbounds broadcasts inbounds list update to all connected clients.
func BroadcastInbounds(inbounds any) {
if hub := GetHub(); hub != nil {
hub.Broadcast(MessageTypeInbounds, inbounds)
}
}
// BroadcastNodes broadcasts the fresh node list to all connected clients.
// Pushed by NodeHeartbeatJob at the end of each 10s tick so the Nodes page
// reflects status / latency / cpu / mem updates without polling.
func BroadcastNodes(nodes any) {
if hub := GetHub(); hub != nil {
hub.Broadcast(MessageTypeNodes, nodes)
}
}
// BroadcastOutbounds broadcasts outbounds list update to all connected clients.
func BroadcastOutbounds(outbounds any) {
if hub := GetHub(); hub != nil {
hub.Broadcast(MessageTypeOutbounds, outbounds)
}
}
// BroadcastNotification broadcasts a system notification to all connected clients.
func BroadcastNotification(title, message, level string) {
hub := GetHub()
if hub == nil {
return
}
hub.Broadcast(MessageTypeNotification, map[string]string{
"title": title,
"message": message,
"level": level,
})
}
// BroadcastXrayState broadcasts Xray state change to all connected clients.
func BroadcastXrayState(state string, errorMsg string) {
hub := GetHub()
if hub == nil {
return
}
hub.Broadcast(MessageTypeXrayState, map[string]string{
"state": state,
"errorMsg": errorMsg,
})
}
// BroadcastInvalidate sends a lightweight signal telling clients to re-fetch
// the named data type via REST. Use this when the caller already knows the
// payload is too large to push directly (e.g., 10k+ clients) to skip the
// JSON-marshal cost on the hot path.
func BroadcastInvalidate(dataType MessageType) {
if hub := GetHub(); hub != nil {
hub.broadcastInvalidate(dataType)
}
}