beam/adapters/ws_conn.rs
1//! Per-connection WebSocket actor — bridges Gun protocol messages over WebSocket.
2//!
3//! [`WsConn`] wraps a single WebSocket connection (either inbound via
4//! [`crate::adapters::WsServer`] or outbound via
5//! [`crate::adapters::OutgoingWebsocketManager`]). It:
6//!
7//! - Sends outgoing messages as `WsMessage::Text` on the WebSocket
8//! - Receives incoming messages, parses them via [`Message::try_from`], and
9//! forwards to the [`crate::router::Router`]
10//! - Sends a `Message::Hi` on startup to register with the router
11//!
12//! # Lifecycle
13//!
14//! 1. `pre_start`: Send `Hi` message, spawn receive loop
15//! 2. `handle`: Forward outgoing messages to WebSocket
16//! 3. `stopping`: Log shutdown
17//!
18//! The receive loop runs as a child task and calls `ctx.stop()` when the
19//! WebSocket closes, triggering actor shutdown.
20
21use crate::actor::{Actor, ActorContext};
22use crate::message::Message;
23use futures_util::{
24 SinkExt, StreamExt,
25 stream::{SplitSink, SplitStream},
26};
27use std::sync::Arc;
28
29use async_trait::async_trait;
30
31use log::{debug, info};
32use web_time::Duration;
33
34use tokio_websockets::{Message as WsMessage, WebSocketStream};
35
36/// A per-connection WebSocket actor that bridges Gun protocol messages.
37///
38/// Created by [`crate::adapters::WsServer`] (inbound) or
39/// [`crate::adapters::OutgoingWebsocketManager`] (outbound). Each `WsConn`
40/// manages a single WebSocket connection and translates between the
41/// Gun wire format (text) and [`Message`] enum values.
42/// A per-connection WebSocket actor that bridges Gun protocol messages.
43///
44/// Generic over the underlying stream type `S` (plain `TcpStream` or
45/// `TlsStream<TcpStream>`). Created by [`crate::adapters::WsServer`]
46/// (inbound) or [`crate::adapters::OutgoingWebsocketManager`] (outbound).
47/// Each `WsConn` manages a single WebSocket connection and translates
48/// between the Gun wire format (text) and [`Message`] enum values.
49pub struct WsConn<S> {
50 /// Write half of the WebSocket (for sending messages in `handle`).
51 ws_sink: Option<SplitSink<WebSocketStream<S>, WsMessage>>,
52 /// Read half of the WebSocket (moved into receive loop in `pre_start`).
53 ws_stream: Option<SplitStream<WebSocketStream<S>>>,
54 allow_public_space: bool,
55 /// Reusable serialization buffer — eliminates per-message allocation.
56 send_buf: Vec<u8>,
57}
58
59impl<S> WsConn<S>
60where
61 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
62{
63 /// Creates a new `WsConn` from a `WebSocketStream`.
64 pub fn new(ws: WebSocketStream<S>, allow_public_space: bool) -> Self {
65 let (sink, stream) = futures_util::StreamExt::split(ws);
66 Self {
67 ws_sink: Some(sink),
68 ws_stream: Some(stream),
69 allow_public_space,
70 send_buf: Vec::with_capacity(512),
71 }
72 }
73
74 /// Serialize a message into `send_buf` and feed it as a WS text frame
75 /// into the sink's internal buffer.
76 ///
77 /// With `flush_threshold(usize::MAX)` configured on the WebSocket
78 /// builder, `feed()` never triggers an implicit flush — it simply
79 /// queues the frame. The caller is responsible for calling
80 /// `flush()` after all messages are queued.
81 async fn send_msg(&mut self, msg: &Arc<Message>, ctx: &ActorContext) {
82 msg.to_writer(&mut self.send_buf);
83 ctx.metrics.record_serialization();
84 if let Some(sink) = &mut self.ws_sink {
85 // Transfer ownership of send_buf into the WS frame — zero copy.
86 // Replace with a fresh buffer for the next message.
87 let buf = std::mem::take(&mut self.send_buf);
88 let _ = sink
89 .feed(WsMessage::text(
90 String::from_utf8(buf).expect("wire format is valid UTF-8"),
91 ))
92 .await;
93 }
94 ctx.metrics.record_ws_sent();
95 }
96
97 /// Flush the WS sink's internal buffer to the underlying TCP stream.
98 async fn flush_sink(&mut self) {
99 if let Some(sink) = &mut self.ws_sink {
100 let _ = sink.flush().await;
101 }
102 }
103}
104
105#[async_trait]
106impl<S> Actor for WsConn<S>
107where
108 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
109{
110 /// Fallback single-message handler. Feeds one message then flushes.
111 /// Used when the actor runtime calls `handle` instead of `handle_batch`.
112 async fn handle(&mut self, msg: Arc<Message>, ctx: &ActorContext) {
113 self.send_msg(&msg, ctx).await;
114 self.flush_sink().await;
115 }
116
117 /// Batch handler — feeds all messages into the WS frame buffer without
118 /// flushing. Because `flush_threshold` is set to `usize::MAX`, `feed()`
119 /// never triggers an implicit flush and never blocks on I/O.
120 ///
121 /// Flushing is handled by a dedicated background task spawned in
122 /// `pre_start`, which calls `flush()` on a timer. This decouples the
123 /// actor's message processing from socket I/O — the actor never
124 /// suspends waiting for the TCP buffer to drain.
125 ///
126 /// # Cooperative Scheduling
127 ///
128 /// On `current_thread` runtime, `feed()` with `flush_threshold(MAX)`
129 /// completes without suspending, so a full batch of 64 messages can
130 /// be processed without yielding to other tasks. We call
131 /// `yield_now()` every 16 messages to ensure the relay's router and
132 /// other actors get scheduled. Without this, on `current_thread`, a
133 /// sender's WsConn can starve the relay's receive loop, causing a
134 /// deadlock where the relay never processes incoming puts.
135 async fn handle_batch(&mut self, batch: &mut Vec<Arc<Message>>, ctx: &ActorContext) {
136 if self.ws_sink.is_some() {
137 let mut count = 0;
138 for msg in batch.drain(..) {
139 self.send_msg(&msg, ctx).await;
140 count += 1;
141 if count % 16 == 0 {
142 crate::tokio_spawn::yield_now().await;
143 }
144 }
145 // Single flush per batch — not per message. This is the key
146 // difference from `sink.send()` (which flushes per message).
147 // One flush per 64 messages dramatically reduces I/O syscalls
148 // while still delivering data to TCP promptly.
149 self.flush_sink().await;
150 } else {
151 batch.clear();
152 }
153 }
154
155 async fn pre_start(&mut self, ctx: &ActorContext) {
156 // Send Hi message to register with the relay.
157 let hi = Message::Hi {
158 from: ctx.addr.clone(),
159 peer_id: ctx.peer_id.read().clone(),
160 };
161 hi.to_writer(&mut self.send_buf);
162 ctx.metrics.record_serialization();
163 if let Some(sink) = &mut self.ws_sink {
164 let buf = std::mem::take(&mut self.send_buf);
165 let _ = sink
166 .feed(WsMessage::text(
167 String::from_utf8(buf).expect("wire format is valid UTF-8"),
168 ))
169 .await;
170 }
171 ctx.metrics.record_ws_sent();
172 self.flush_sink().await;
173
174 // Move the read half into a child task for the receive loop.
175 let reader = self.ws_stream.take().expect("ws_stream already taken");
176 let ctx2 = ctx.clone();
177 let allow_public_space = self.allow_public_space;
178 ctx.child_task(async move {
179 let mut reader = reader;
180 while let Some(result) = reader.next().await {
181 let ws_msg = match result {
182 Ok(m) => m,
183 Err(_e) => {
184 break;
185 }
186 };
187 if ws_msg.is_text() {
188 let text = ws_msg.as_text().unwrap_or("");
189 if text.is_empty() {
190 continue;
191 }
192 ctx2.metrics.record_ws_received();
193 match Message::try_from(text, ctx2.addr.clone(), allow_public_space) {
194 Ok(msgs) => {
195 ctx2.metrics.record_parsed();
196 for msg in msgs {
197 let _ = ctx2.router.read().send(msg);
198 }
199 }
200 Err(e) => {
201 debug!("[WS] parse error: {} (len={})", e, text.len());
202 }
203 }
204 } else if ws_msg.is_binary() {
205 debug!("[WS] binary frame (ignored)");
206 } else if ws_msg.is_close() {
207 debug!("[WS] close frame received from peer");
208 break;
209 } else if ws_msg.is_ping() {
210 debug!("[WS] ping frame (ignored)");
211 } else if ws_msg.is_pong() {
212 debug!("[WS] pong frame (ignored)");
213 }
214 }
215 debug!("[WS] receive loop ended — stopping actor");
216 ctx2.stop();
217 });
218 }
219
220 async fn stopping(&mut self, _context: &ActorContext) {
221 info!("WsConn stopping — sending WebSocket Close frame");
222 if let Some(sink) = &mut self.ws_sink {
223 let close_result =
224 crate::tokio_time::timeout(Duration::from_secs(2), sink.close()).await;
225 match close_result {
226 Ok(Ok(())) => debug!("WsConn Close frame acknowledged"),
227 Ok(Err(e)) => debug!("WsConn Close error (non-fatal): {}", e),
228 Err(_) => debug!("WsConn Close timed out — connection dropped"),
229 }
230 }
231 }
232}