Skip to main content

ply_engine/net/
websocket.rs

1#[cfg(target_arch = "wasm32")]
2use sapp_jsutils::JsObject;
3
4/// Configuration builder passed to the WebSocket connect closure.
5pub struct WsConfig {
6    pub(crate) headers: Vec<(String, String)>,
7    pub(crate) insecure: bool,
8}
9
10impl WsConfig {
11    pub(crate) fn new() -> Self {
12        Self {
13            headers: Vec::new(),
14            insecure: false,
15        }
16    }
17
18    /// Add a header to the connection handshake.
19    pub fn header(&mut self, key: &str, value: &str) -> &mut Self {
20        self.headers.push((key.to_owned(), value.to_owned()));
21        self
22    }
23
24    /// Disable TLS certificate verification (for dev servers).
25    /// Note: has no effect on WASM — browser handles TLS.
26    pub fn insecure(&mut self) -> &mut Self {
27        self.insecure = true;
28        self
29    }
30}
31
32/// A message received from a WebSocket.
33#[derive(Debug, Clone)]
34pub enum WsMessage {
35    /// Connection established.
36    Connected,
37    /// Text frame received.
38    Text(String),
39    /// Binary frame received.
40    Binary(Vec<u8>),
41    /// An error occurred.
42    Error(String),
43    /// The connection was closed.
44    Closed,
45}
46
47/// Outgoing message sent to the background task.
48#[cfg(not(target_arch = "wasm32"))]
49pub(crate) enum OutgoingWsMessage {
50    Text(String),
51    Binary(Vec<u8>),
52    Close,
53}
54
55/// A thin handle to a WebSocket tracked by the global manager.
56///
57/// Does not own the data — just a key for lookups.
58pub struct WebSocket {
59    pub(crate) id: u64,
60}
61
62impl WebSocket {
63    /// Send binary data.
64    pub fn send(&self, data: &[u8]) {
65        let mgr = super::NET_MANAGER.lock().unwrap();
66        if let Some(entry) = mgr.websockets.get(&self.id) {
67            entry.state.send_binary(data);
68        }
69    }
70
71    /// Send a text message.
72    pub fn send_text(&self, text: &str) {
73        let mgr = super::NET_MANAGER.lock().unwrap();
74        if let Some(entry) = mgr.websockets.get(&self.id) {
75            entry.state.send_text(text);
76        }
77    }
78
79    /// Pop the next incoming message.
80    ///
81    /// Use `while let Some(msg) = ws.recv()` to drain all messages
82    /// that arrived since the last frame.
83    pub fn recv(&self) -> Option<WsMessage> {
84        let mut mgr = super::NET_MANAGER.lock().unwrap();
85        let entry = mgr.websockets.get_mut(&self.id)?;
86        entry.frames_not_accessed = 0;
87        entry.state.try_recv()
88    }
89
90    /// Graceful close. Consumes the handle and removes the entry immediately.
91    pub fn close(self) {
92        let mut mgr = super::NET_MANAGER.lock().unwrap();
93        if let Some(entry) = mgr.websockets.get(&self.id) {
94            entry.state.close();
95        }
96        mgr.websockets.remove(&self.id);
97    }
98}
99
100/// Internal state for a tracked WebSocket (native).
101#[cfg(not(target_arch = "wasm32"))]
102pub(crate) struct WebSocketState {
103    pub tx: tokio::sync::mpsc::UnboundedSender<OutgoingWsMessage>,
104    pub rx: std::sync::mpsc::Receiver<WsMessage>,
105    pub _runtime: tokio::runtime::Runtime,
106}
107
108#[cfg(not(target_arch = "wasm32"))]
109impl WebSocketState {
110    pub fn send_binary(&self, data: &[u8]) {
111        let _ = self.tx.send(OutgoingWsMessage::Binary(data.to_vec()));
112    }
113
114    pub fn send_text(&self, text: &str) {
115        let _ = self.tx.send(OutgoingWsMessage::Text(text.to_owned()));
116    }
117
118    pub fn try_recv(&self) -> Option<WsMessage> {
119        self.rx.try_recv().ok()
120    }
121
122    pub fn close(&self) {
123        let _ = self.tx.send(OutgoingWsMessage::Close);
124    }
125
126    pub fn is_disconnected(&self) -> bool {
127        self.tx.is_closed()
128    }
129}
130
131/// Internal state for a tracked WebSocket (WASM).
132#[cfg(target_arch = "wasm32")]
133pub(crate) struct WebSocketState {
134    /// The integer socket ID used by the JS bridge.
135    pub socket_id: i32,
136}
137
138#[cfg(target_arch = "wasm32")]
139impl WebSocketState {
140    pub fn send_binary(&self, data: &[u8]) {
141        unsafe {
142            ply_net_ws_send_binary(self.socket_id, JsObject::buffer(data));
143        }
144    }
145
146    pub fn send_text(&self, text: &str) {
147        unsafe {
148            ply_net_ws_send_text(self.socket_id, JsObject::string(text));
149        }
150    }
151
152    pub fn try_recv(&self) -> Option<WsMessage> {
153        let js_obj = unsafe { ply_net_ws_try_recv(self.socket_id) };
154        if js_obj.is_nil() {
155            return None;
156        }
157
158        let type_id = js_obj.field_u32("type");
159        match type_id {
160            0 => Some(WsMessage::Connected),   // PLY_WS_CONNECTED
161            1 => {                              // PLY_WS_BINARY
162                let mut buf = Vec::new();
163                js_obj.field("data").to_byte_buffer(&mut buf);
164                Some(WsMessage::Binary(buf))
165            }
166            2 => {                              // PLY_WS_TEXT
167                let mut text = String::new();
168                js_obj.field("data").to_string(&mut text);
169                Some(WsMessage::Text(text))
170            }
171            3 => {                              // PLY_WS_ERROR
172                let mut err = String::new();
173                js_obj.field("data").to_string(&mut err);
174                Some(WsMessage::Error(err))
175            }
176            4 => Some(WsMessage::Closed),       // PLY_WS_CLOSED
177            _ => None,
178        }
179    }
180
181    pub fn close(&self) {
182        unsafe {
183            ply_net_ws_close(self.socket_id);
184        }
185    }
186
187    /// On WASM, we don't have a channel — we consider it disconnected
188    /// if the JS side has removed the socket (close was called).
189    pub fn is_disconnected(&self) -> bool {
190        // After close() is called, the JS entry is deleted.
191        // We rely on the immediate removal in WebSocket::close(self).
192        false
193    }
194}
195
196// WASM FFI
197#[cfg(target_arch = "wasm32")]
198extern "C" {
199    pub(crate) fn ply_net_ws_connect(socket_id: i32, addr: JsObject);
200    fn ply_net_ws_send_binary(socket_id: i32, data: JsObject);
201    fn ply_net_ws_send_text(socket_id: i32, text: JsObject);
202    fn ply_net_ws_close(socket_id: i32);
203    fn ply_net_ws_try_recv(socket_id: i32) -> JsObject;
204}