1use std::collections::{HashMap, HashSet, VecDeque};
24use std::sync::Arc;
25
26use tokio::sync::{mpsc, oneshot, Mutex};
27
28use authsocket::client::AuthSocketClient;
29use bsv::wallet::interfaces::WalletInterface;
30
31use crate::encryption;
32use crate::error::MessageBoxError;
33use crate::types::{AuthenticatedPeerMessage, ServerPeerMessage};
34
35type SubscriptionMap =
37 Arc<Mutex<HashMap<String, Arc<dyn Fn(AuthenticatedPeerMessage) + Send + Sync>>>>;
38
39type PendingAcks = Arc<Mutex<HashMap<String, VecDeque<oneshot::Sender<bool>>>>>;
48
49pub struct MessageBoxWebSocket {
52 ws: AuthSocketClient,
54 subscriptions: SubscriptionMap,
56 pending_acks: PendingAcks,
58 joined_rooms: Arc<Mutex<HashSet<String>>>,
60}
61
62impl MessageBoxWebSocket {
63 pub async fn connect<W>(
69 url: &str,
70 identity_key: &str,
71 wallet: W,
72 originator: Option<String>,
73 ) -> Result<Self, MessageBoxError>
74 where
75 W: WalletInterface + Clone + Send + Sync + 'static,
76 {
77 let ws = AuthSocketClient::connect(url, identity_key, wallet.clone())
78 .await
79 .map_err(|e| MessageBoxError::WebSocket(e.to_string()))?;
80
81 let subscriptions: SubscriptionMap = Arc::new(Mutex::new(HashMap::new()));
82 let pending_acks: PendingAcks = Arc::new(Mutex::new(HashMap::new()));
83 let joined_rooms: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
84
85 let (event_tx, mut event_rx) = mpsc::unbounded_channel::<(String, serde_json::Value)>();
89 ws.set_fallback(Arc::new(move |event_name, data| {
90 let _ = event_tx.send((event_name, data));
91 }))
92 .await;
93
94 {
98 let subscriptions = subscriptions.clone();
99 let pending_acks = pending_acks.clone();
100 let wallet = wallet.clone();
101 let originator = originator.clone();
102 tokio::spawn(async move {
103 while let Some((event_name, data)) = event_rx.recv().await {
104 if let Some(room_id) = event_name.strip_prefix("sendMessage-") {
105 let room_id = room_id.to_string();
106 let Ok(server_msg) =
107 serde_json::from_value::<ServerPeerMessage>(data.clone())
108 else {
109 continue;
110 };
111 let callback = {
112 let guard = subscriptions.lock().await;
113 guard.get(&event_name).cloned()
114 };
115 if let Some(cb) = callback {
116 let outcome = encryption::try_decrypt_message_typed(
120 &wallet,
121 &server_msg.body,
122 &server_msg.sender,
123 originator.as_deref(),
124 )
125 .await;
126 let authenticated_decrypt = outcome.is_authenticated();
127 let (recipient, message_box) = split_room_id(&room_id);
128 cb(AuthenticatedPeerMessage {
129 message_id: server_msg.message_id,
130 sender: server_msg.sender,
131 recipient,
132 message_box,
133 body: outcome.into_body(),
134 authenticated_decrypt,
135 });
136 }
137 } else if event_name.starts_with("sendMessageAck-") {
138 let success =
141 data.get("status").and_then(|s| s.as_str()) == Some("success");
142 let mut guard = pending_acks.lock().await;
143 if let Some(queue) = guard.get_mut(&event_name) {
144 if let Some(tx) = queue.pop_front() {
145 let _ = tx.send(success);
146 }
147 if queue.is_empty() {
148 guard.remove(&event_name);
149 }
150 }
151 }
152 }
155 });
156 }
157
158 Ok(Self {
159 ws,
160 subscriptions,
161 pending_acks,
162 joined_rooms,
163 })
164 }
165
166 pub fn is_connected(&self) -> bool {
168 self.ws.is_connected()
169 }
170
171 pub fn ms_since_last_inbound(&self) -> u64 {
174 self.ws.ms_since_last_inbound()
175 }
176
177 pub fn server_identity_key(&self) -> &str {
179 self.ws.server_identity_key()
180 }
181
182 pub async fn join_room(&self, room_id: &str) -> Result<(), MessageBoxError> {
186 {
187 let guard = self.joined_rooms.lock().await;
188 if guard.contains(room_id) {
189 return Ok(());
190 }
191 }
192 self.ws
193 .join_room(room_id)
194 .await
195 .map_err(|e| MessageBoxError::WebSocket(e.to_string()))?;
196 self.joined_rooms.lock().await.insert(room_id.to_string());
197 Ok(())
198 }
199
200 pub async fn leave_room(&self, room_id: &str) -> Result<(), MessageBoxError> {
205 self.joined_rooms.lock().await.remove(room_id);
206 let event_key = format!("sendMessage-{room_id}");
207 self.subscriptions.lock().await.remove(&event_key);
208 self.ws
209 .leave_room(room_id)
210 .await
211 .map_err(|e| MessageBoxError::WebSocket(e.to_string()))
212 }
213
214 pub async fn subscribe(
218 &self,
219 event_key: String,
220 callback: Arc<dyn Fn(AuthenticatedPeerMessage) + Send + Sync>,
221 ) {
222 self.subscriptions.lock().await.insert(event_key, callback);
223 }
224
225 pub async fn emit_send_message(
232 &self,
233 payload: serde_json::Value,
234 ack_key: String,
235 ack_tx: oneshot::Sender<bool>,
236 ) -> Result<(), MessageBoxError> {
237 self.pending_acks
240 .lock()
241 .await
242 .entry(ack_key.clone())
243 .or_default()
244 .push_back(ack_tx);
245
246 if let Err(e) = self.ws.emit("sendMessage", &payload).await {
247 let mut guard = self.pending_acks.lock().await;
250 if let Some(queue) = guard.get_mut(&ack_key) {
251 queue.pop_back();
252 if queue.is_empty() {
253 guard.remove(&ack_key);
254 }
255 }
256 return Err(MessageBoxError::WebSocket(e.to_string()));
257 }
258 Ok(())
259 }
260
261 pub async fn remove_pending_ack(&self, key: &str) {
266 let mut guard = self.pending_acks.lock().await;
267 if let Some(queue) = guard.get_mut(key) {
268 queue.pop_front();
269 if queue.is_empty() {
270 guard.remove(key);
271 }
272 }
273 }
274
275 pub async fn disconnect(&self) -> Result<(), MessageBoxError> {
279 {
280 let mut guard = self.pending_acks.lock().await;
281 for (_, queue) in guard.drain() {
282 for tx in queue {
283 let _ = tx.send(false);
284 }
285 }
286 }
287 self.subscriptions.lock().await.clear();
288 self.joined_rooms.lock().await.clear();
289 self.ws
290 .disconnect()
291 .await
292 .map_err(|e| MessageBoxError::WebSocket(e.to_string()))
293 }
294}
295
296fn split_room_id(room_id: &str) -> (String, String) {
302 const HEX_KEY_LEN: usize = 66;
303 if room_id.len() > HEX_KEY_LEN && room_id.as_bytes()[HEX_KEY_LEN] == b'-' {
304 let key = room_id[..HEX_KEY_LEN].to_string();
305 let mb = room_id[HEX_KEY_LEN + 1..].to_string();
306 return (key, mb);
307 }
308 if let Some(pos) = room_id.find('-') {
309 (room_id[..pos].to_string(), room_id[pos + 1..].to_string())
310 } else {
311 (room_id.to_string(), String::new())
312 }
313}
314
315#[cfg(test)]
320mod tests {
321 use super::*;
322 use crate::types::{WsSendMessageData, WsSendMessagePayload};
323 use serde_json::json;
324 use std::collections::HashSet;
325
326 #[test]
329 fn room_id_format() {
330 let my_key = "03abc";
331 let recipient = "03def";
332 let message_box = "payment_inbox";
333
334 let listen_room = format!("{my_key}-{message_box}");
335 let send_room = format!("{recipient}-{message_box}");
336
337 assert_eq!(listen_room, "03abc-payment_inbox");
338 assert_eq!(send_room, "03def-payment_inbox");
339 assert_ne!(listen_room, send_room);
340 }
341
342 #[test]
344 fn send_message_data_serializes_camel_case() {
345 let data = WsSendMessageData {
346 room_id: "03abc-payment_inbox".to_string(),
347 message: WsSendMessagePayload {
348 message_id: "deadbeef".to_string(),
349 recipient: "03abc".to_string(),
350 body: "encrypted".to_string(),
351 },
352 };
353 let json = serde_json::to_string(&data).unwrap();
354 assert!(json.contains("\"roomId\""), "roomId field name");
355 assert!(json.contains("\"messageId\""), "messageId field name");
356 assert!(!json.contains("room_id"), "no snake_case leakage");
357 assert!(!json.contains("message_id"), "no snake_case leakage");
358 }
359
360 #[test]
362 fn send_message_payload_round_trip() {
363 let payload = WsSendMessagePayload {
364 message_id: "abc123".to_string(),
365 recipient: "03def456".to_string(),
366 body: r#"{"encryptedMessage":"abc=="}"#.to_string(),
367 };
368 let json = serde_json::to_string(&payload).unwrap();
369 let back: WsSendMessagePayload = serde_json::from_str(&json).unwrap();
370 assert_eq!(back.message_id, "abc123");
371 assert_eq!(back.recipient, "03def456");
372 assert_eq!(back.body, r#"{"encryptedMessage":"abc=="}"#);
373 }
374
375 #[test]
377 fn authenticated_event_format() {
378 let identity_key = "03abcdef1234567890";
379 let v = json!({"identityKey": identity_key});
380 let json = serde_json::to_string(&v).unwrap();
381 assert_eq!(json, r#"{"identityKey":"03abcdef1234567890"}"#);
382 }
383
384 #[test]
386 fn join_room_idempotency_uses_hashset() {
387 let mut rooms: HashSet<String> = HashSet::new();
388 let room_id = "03abc-payment_inbox";
389 let first = rooms.insert(room_id.to_string());
390 let second = rooms.insert(room_id.to_string());
391 assert!(first, "first insert returns true");
392 assert!(!second, "second insert returns false (already present)");
393 assert_eq!(rooms.len(), 1, "only one entry in set");
394 }
395
396 #[test]
398 fn split_room_id_hex_key() {
399 let key = "a".repeat(66);
400 let mb = "my_inbox";
401 let room_id = format!("{key}-{mb}");
402 let (got_key, got_mb) = split_room_id(&room_id);
403 assert_eq!(got_key, key);
404 assert_eq!(got_mb, mb);
405 }
406
407 #[test]
409 fn split_room_id_mb_with_hyphen() {
410 let key = "b".repeat(66);
411 let mb = "payment-inbox-v2";
412 let room_id = format!("{key}-{mb}");
413 let (got_key, got_mb) = split_room_id(&room_id);
414 assert_eq!(got_key, key);
415 assert_eq!(got_mb, mb);
416 }
417
418 #[tokio::test]
425 async fn fifo_acks_resolve_concurrent_same_room_sends_in_order() {
426 let acks: PendingAcks = Arc::new(Mutex::new(HashMap::new()));
427 let key = "sendMessageAck-03abc-inbox".to_string();
428
429 let (tx1, rx1) = oneshot::channel::<bool>();
430 let (tx2, rx2) = oneshot::channel::<bool>();
431 {
432 let mut g = acks.lock().await;
433 g.entry(key.clone()).or_default().push_back(tx1);
434 g.entry(key.clone()).or_default().push_back(tx2);
435 assert_eq!(g.get(&key).unwrap().len(), 2, "both waiters queued");
436 }
437
438 {
439 let mut g = acks.lock().await;
440 let q = g.get_mut(&key).unwrap();
441 let _ = q.pop_front().unwrap().send(true);
442 assert!(!q.is_empty(), "second waiter still queued");
443 }
444 assert!(rx1.await.unwrap(), "first send resolved by first ack");
445
446 {
447 let mut g = acks.lock().await;
448 let q = g.get_mut(&key).unwrap();
449 let _ = q.pop_front().unwrap().send(false);
450 if q.is_empty() {
451 g.remove(&key);
452 }
453 assert!(!g.contains_key(&key), "key removed once queue drains");
454 }
455 assert!(!rx2.await.unwrap(), "second send resolved by second ack");
456 }
457
458 #[tokio::test]
461 async fn remove_pending_ack_pops_oldest_and_clears_empty_key() {
462 let acks: PendingAcks = Arc::new(Mutex::new(HashMap::new()));
463 let key = "sendMessageAck-03abc-inbox".to_string();
464 let (tx, _rx) = oneshot::channel::<bool>();
465 acks.lock().await.entry(key.clone()).or_default().push_back(tx);
466
467 {
468 let mut g = acks.lock().await;
469 if let Some(q) = g.get_mut(&key) {
470 q.pop_front();
471 if q.is_empty() {
472 g.remove(&key);
473 }
474 }
475 }
476 assert!(acks.lock().await.is_empty(), "no leaked ack entries");
477 }
478
479 #[tokio::test]
481 async fn acks_are_per_room_independent() {
482 let acks: PendingAcks = Arc::new(Mutex::new(HashMap::new()));
483 let key_a = "sendMessageAck-03aaa-inbox".to_string();
484 let key_b = "sendMessageAck-03bbb-inbox".to_string();
485 let (tx_a, rx_a) = oneshot::channel::<bool>();
486 let (tx_b, rx_b) = oneshot::channel::<bool>();
487 {
488 let mut g = acks.lock().await;
489 g.entry(key_a.clone()).or_default().push_back(tx_a);
490 g.entry(key_b.clone()).or_default().push_back(tx_b);
491 }
492 {
493 let mut g = acks.lock().await;
494 let q = g.get_mut(&key_a).unwrap();
495 let _ = q.pop_front().unwrap().send(true);
496 if q.is_empty() {
497 g.remove(&key_a);
498 }
499 }
500 assert!(rx_a.await.unwrap(), "room A resolved");
501 assert!(acks.lock().await.contains_key(&key_b), "room B still pending");
502 drop(rx_b);
503 }
504}