feat(web-gateway,web-dashboard): WS ping/pong keepalive, vite build config

- Respond to WebSocket Ping frames with Pong for keepalive detection
- Refactor WS handler to share sender between broadcast and recv tasks
  via Arc<Mutex> (required for pong responses from recv task)
- Add vite production build config: source maps, manual chunk splitting
  for vendor/charts/query bundles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-22 08:36:28 +01:00
parent 544315e351
commit 3d3a84e0b0
2 changed files with 28 additions and 6 deletions

View File

@@ -12,4 +12,16 @@ export default defineConfig({
},
},
},
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
charts: ['lightweight-charts', 'recharts'],
query: ['@tanstack/react-query'],
},
},
},
},
})

View File

@@ -48,7 +48,7 @@ fn validate_ws_token(token: &str, config: &Arc<GatewayConfig>) -> Result<Claims,
/// Handle an established WebSocket connection
async fn handle_ws_connection(socket: WebSocket, state: AppState) {
let (mut ws_sender, mut ws_receiver) = socket.split();
let (ws_sender, mut ws_receiver) = socket.split();
// Subscribe to the broadcast channel for server events
let mut broadcast_rx = state.ws_broadcast.subscribe();
@@ -59,6 +59,11 @@ async fn handle_ws_connection(socket: WebSocket, state: AppState) {
let topics_for_broadcast = subscribed_topics.clone();
// Shared sender for both broadcast forwarding and pong responses
let sender = Arc::new(tokio::sync::Mutex::new(ws_sender));
let sender_for_broadcast = sender.clone();
let sender_for_recv = sender.clone();
// Task: forward broadcast events to WebSocket client (filtered by subscribed topics)
let mut send_task = tokio::spawn(async move {
while let Ok(msg_json) = broadcast_rx.recv().await {
@@ -74,14 +79,15 @@ async fn handle_ws_connection(socket: WebSocket, state: AppState) {
};
if should_send {
if ws_sender.send(Message::Text(msg_json.into())).await.is_err() {
let mut s = sender_for_broadcast.lock().await;
if s.send(Message::Text(msg_json.into())).await.is_err() {
break; // Client disconnected
}
}
}
});
// Task: receive client messages (subscribe/unsubscribe)
// Task: receive client messages (subscribe/unsubscribe) and respond to pings
let topics_for_recv = subscribed_topics.clone();
let mut recv_task = tokio::spawn(async move {
while let Some(Ok(msg)) = ws_receiver.next().await {
@@ -106,10 +112,14 @@ async fn handle_ws_connection(socket: WebSocket, state: AppState) {
}
// Silently ignore malformed messages
}
Message::Close(_) => break,
Message::Binary(_) | Message::Ping(_) | Message::Pong(_) => {
// Ignore binary frames and WebSocket control frames
Message::Ping(data) => {
let mut s = sender_for_recv.lock().await;
if s.send(Message::Pong(data)).await.is_err() {
break;
}
}
Message::Close(_) => break,
Message::Binary(_) | Message::Pong(_) => {}
}
}
});