mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-04 03:44:22 +00:00
fc5be5b9e4
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.
116 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|