1#[cfg(feature = "native")]
13use tokio::sync::Mutex;
14
15use tracing::debug;
16
17use clawft_types::error::ClawftError;
18use clawft_types::event::{InboundMessage, OutboundMessage};
19
20#[cfg(feature = "native")]
22const DEFAULT_CHANNEL_CAPACITY: usize = 1024;
23
24#[cfg(feature = "native")]
29pub struct MessageBus {
30 inbound_tx: tokio::sync::mpsc::Sender<InboundMessage>,
31 inbound_rx: Mutex<tokio::sync::mpsc::Receiver<InboundMessage>>,
32 outbound_tx: tokio::sync::mpsc::Sender<OutboundMessage>,
33 outbound_rx: Mutex<tokio::sync::mpsc::Receiver<OutboundMessage>>,
34}
35
36#[cfg(feature = "native")]
37impl MessageBus {
38 pub fn new() -> Self {
40 Self::with_capacity(DEFAULT_CHANNEL_CAPACITY)
41 }
42
43 pub fn with_capacity(capacity: usize) -> Self {
48 let (inbound_tx, inbound_rx) = tokio::sync::mpsc::channel(capacity);
49 let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(capacity);
50
51 debug!(capacity, "MessageBus created with bounded channels");
52
53 Self {
54 inbound_tx,
55 inbound_rx: Mutex::new(inbound_rx),
56 outbound_tx,
57 outbound_rx: Mutex::new(outbound_rx),
58 }
59 }
60
61 pub fn publish_inbound(&self, msg: InboundMessage) -> Result<(), ClawftError> {
66 debug!(
67 channel = %msg.channel,
68 chat_id = %msg.chat_id,
69 "publishing inbound message"
70 );
71 self.inbound_tx.try_send(msg).map_err(|e| match e {
72 tokio::sync::mpsc::error::TrySendError::Full(_) => {
73 ClawftError::Channel("inbound channel full (backpressure)".into())
74 }
75 tokio::sync::mpsc::error::TrySendError::Closed(_) => {
76 ClawftError::Channel("inbound channel closed".into())
77 }
78 })
79 }
80
81 pub async fn publish_inbound_async(
86 &self,
87 msg: InboundMessage,
88 ) -> Result<(), ClawftError> {
89 debug!(
90 channel = %msg.channel,
91 chat_id = %msg.chat_id,
92 "publishing inbound message (async)"
93 );
94 self.inbound_tx
95 .send(msg)
96 .await
97 .map_err(|e| ClawftError::Channel(format!("inbound channel closed: {e}")))
98 }
99
100 pub async fn consume_inbound(&self) -> Option<InboundMessage> {
105 let mut rx = self.inbound_rx.lock().await;
106 rx.recv().await
107 }
108
109 pub fn dispatch_outbound(&self, msg: OutboundMessage) -> Result<(), ClawftError> {
114 debug!(
115 channel = %msg.channel,
116 chat_id = %msg.chat_id,
117 "dispatching outbound message"
118 );
119 self.outbound_tx.try_send(msg).map_err(|e| match e {
120 tokio::sync::mpsc::error::TrySendError::Full(_) => {
121 ClawftError::Channel("outbound channel full (backpressure)".into())
122 }
123 tokio::sync::mpsc::error::TrySendError::Closed(_) => {
124 ClawftError::Channel("outbound channel closed".into())
125 }
126 })
127 }
128
129 pub async fn dispatch_outbound_async(
131 &self,
132 msg: OutboundMessage,
133 ) -> Result<(), ClawftError> {
134 debug!(
135 channel = %msg.channel,
136 chat_id = %msg.chat_id,
137 "dispatching outbound message (async)"
138 );
139 self.outbound_tx
140 .send(msg)
141 .await
142 .map_err(|e| ClawftError::Channel(format!("outbound channel closed: {e}")))
143 }
144
145 pub async fn consume_outbound(&self) -> Option<OutboundMessage> {
150 let mut rx = self.outbound_rx.lock().await;
151 rx.recv().await
152 }
153
154 pub fn inbound_sender(&self) -> tokio::sync::mpsc::Sender<InboundMessage> {
159 self.inbound_tx.clone()
160 }
161
162 pub fn outbound_sender(&self) -> tokio::sync::mpsc::Sender<OutboundMessage> {
167 self.outbound_tx.clone()
168 }
169}
170
171#[cfg(not(feature = "native"))]
176pub struct MessageBus {
177 inbound_tx: futures_util::lock::Mutex<futures_channel::mpsc::UnboundedSender<InboundMessage>>,
178 inbound_rx: futures_util::lock::Mutex<futures_channel::mpsc::UnboundedReceiver<InboundMessage>>,
179 outbound_tx: futures_util::lock::Mutex<futures_channel::mpsc::UnboundedSender<OutboundMessage>>,
180 outbound_rx: futures_util::lock::Mutex<futures_channel::mpsc::UnboundedReceiver<OutboundMessage>>,
181}
182
183#[cfg(not(feature = "native"))]
184impl MessageBus {
185 pub fn new() -> Self {
187 let (inbound_tx, inbound_rx) = futures_channel::mpsc::unbounded();
188 let (outbound_tx, outbound_rx) = futures_channel::mpsc::unbounded();
189
190 debug!("MessageBus created with unbounded channels (browser)");
191
192 Self {
193 inbound_tx: futures_util::lock::Mutex::new(inbound_tx),
194 inbound_rx: futures_util::lock::Mutex::new(inbound_rx),
195 outbound_tx: futures_util::lock::Mutex::new(outbound_tx),
196 outbound_rx: futures_util::lock::Mutex::new(outbound_rx),
197 }
198 }
199
200 pub fn with_capacity(_capacity: usize) -> Self {
202 Self::new()
203 }
204
205 pub fn publish_inbound(&self, msg: InboundMessage) -> Result<(), ClawftError> {
207 debug!(
208 channel = %msg.channel,
209 chat_id = %msg.chat_id,
210 "publishing inbound message"
211 );
212 if let Some(tx) = self.inbound_tx.try_lock() {
214 tx.unbounded_send(msg).map_err(|_| {
215 ClawftError::Channel("inbound channel closed".into())
216 })
217 } else {
218 Err(ClawftError::Channel("inbound channel busy".into()))
219 }
220 }
221
222 pub async fn publish_inbound_async(
224 &self,
225 msg: InboundMessage,
226 ) -> Result<(), ClawftError> {
227 debug!(
228 channel = %msg.channel,
229 chat_id = %msg.chat_id,
230 "publishing inbound message (async)"
231 );
232 let tx = self.inbound_tx.lock().await;
233 tx.unbounded_send(msg).map_err(|_| {
234 ClawftError::Channel("inbound channel closed".into())
235 })
236 }
237
238 pub async fn consume_inbound(&self) -> Option<InboundMessage> {
240 use futures_util::StreamExt;
241 let mut rx = self.inbound_rx.lock().await;
242 rx.next().await
243 }
244
245 pub fn dispatch_outbound(&self, msg: OutboundMessage) -> Result<(), ClawftError> {
247 debug!(
248 channel = %msg.channel,
249 chat_id = %msg.chat_id,
250 "dispatching outbound message"
251 );
252 if let Some(tx) = self.outbound_tx.try_lock() {
253 tx.unbounded_send(msg).map_err(|_| {
254 ClawftError::Channel("outbound channel closed".into())
255 })
256 } else {
257 Err(ClawftError::Channel("outbound channel busy".into()))
258 }
259 }
260
261 pub async fn dispatch_outbound_async(
263 &self,
264 msg: OutboundMessage,
265 ) -> Result<(), ClawftError> {
266 debug!(
267 channel = %msg.channel,
268 chat_id = %msg.chat_id,
269 "dispatching outbound message (async)"
270 );
271 let tx = self.outbound_tx.lock().await;
272 tx.unbounded_send(msg).map_err(|_| {
273 ClawftError::Channel("outbound channel closed".into())
274 })
275 }
276
277 pub async fn consume_outbound(&self) -> Option<OutboundMessage> {
279 use futures_util::StreamExt;
280 let mut rx = self.outbound_rx.lock().await;
281 rx.next().await
282 }
283}
284
285impl Default for MessageBus {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use chrono::Utc;
295 use std::collections::HashMap;
296
297 fn make_inbound(content: &str) -> InboundMessage {
298 InboundMessage {
299 channel: "test".into(),
300 sender_id: "user1".into(),
301 chat_id: "chat1".into(),
302 content: content.into(),
303 timestamp: Utc::now(),
304 media: vec![],
305 metadata: HashMap::new(),
306 }
307 }
308
309 fn make_outbound(content: &str) -> OutboundMessage {
310 OutboundMessage {
311 channel: "test".into(),
312 chat_id: "chat1".into(),
313 content: content.into(),
314 reply_to: None,
315 media: vec![],
316 metadata: HashMap::new(),
317 }
318 }
319
320 #[tokio::test]
321 async fn publish_and_consume_inbound() {
322 let bus = MessageBus::new();
323 let msg = make_inbound("hello");
324 bus.publish_inbound(msg).unwrap();
325
326 let received = bus.consume_inbound().await.unwrap();
327 assert_eq!(received.content, "hello");
328 assert_eq!(received.channel, "test");
329 }
330
331 #[tokio::test]
332 async fn dispatch_and_consume_outbound() {
333 let bus = MessageBus::new();
334 let msg = make_outbound("reply");
335 bus.dispatch_outbound(msg).unwrap();
336
337 let received = bus.consume_outbound().await.unwrap();
338 assert_eq!(received.content, "reply");
339 }
340
341 #[tokio::test]
342 async fn multiple_inbound_messages_in_order() {
343 let bus = MessageBus::new();
344 for i in 0..5 {
345 bus.publish_inbound(make_inbound(&format!("msg-{i}")))
346 .unwrap();
347 }
348
349 for i in 0..5 {
350 let msg = bus.consume_inbound().await.unwrap();
351 assert_eq!(msg.content, format!("msg-{i}"));
352 }
353 }
354
355 #[tokio::test]
356 async fn multiple_outbound_messages_in_order() {
357 let bus = MessageBus::new();
358 for i in 0..5 {
359 bus.dispatch_outbound(make_outbound(&format!("out-{i}")))
360 .unwrap();
361 }
362
363 for i in 0..5 {
364 let msg = bus.consume_outbound().await.unwrap();
365 assert_eq!(msg.content, format!("out-{i}"));
366 }
367 }
368
369 #[tokio::test]
370 async fn default_creates_valid_bus() {
371 let bus = MessageBus::default();
372 bus.publish_inbound(make_inbound("default-test")).unwrap();
373 bus.dispatch_outbound(make_outbound("default-test"))
374 .unwrap();
375 }
376
377 #[tokio::test]
378 async fn inbound_and_outbound_are_independent() {
379 let bus = MessageBus::new();
380
381 bus.publish_inbound(make_inbound("in")).unwrap();
382 bus.dispatch_outbound(make_outbound("out")).unwrap();
383
384 let inbound = bus.consume_inbound().await.unwrap();
385 let outbound = bus.consume_outbound().await.unwrap();
386
387 assert_eq!(inbound.content, "in");
388 assert_eq!(outbound.content, "out");
389 }
390
391 #[cfg(feature = "native")]
395 #[tokio::test]
396 async fn inbound_sender_allows_multi_producer() {
397 let bus = MessageBus::new();
398 let tx1 = bus.inbound_sender();
399 let tx2 = bus.inbound_sender();
400
401 tx1.try_send(make_inbound("from-tx1")).unwrap();
402 tx2.try_send(make_inbound("from-tx2")).unwrap();
403
404 let msg1 = bus.consume_inbound().await.unwrap();
405 let msg2 = bus.consume_inbound().await.unwrap();
406 assert_eq!(msg1.content, "from-tx1");
407 assert_eq!(msg2.content, "from-tx2");
408 }
409
410 #[cfg(feature = "native")]
411 #[tokio::test]
412 async fn outbound_sender_allows_multi_producer() {
413 let bus = MessageBus::new();
414 let tx1 = bus.outbound_sender();
415 let tx2 = bus.outbound_sender();
416
417 tx1.try_send(make_outbound("from-tx1")).unwrap();
418 tx2.try_send(make_outbound("from-tx2")).unwrap();
419
420 let msg1 = bus.consume_outbound().await.unwrap();
421 let msg2 = bus.consume_outbound().await.unwrap();
422 assert_eq!(msg1.content, "from-tx1");
423 assert_eq!(msg2.content, "from-tx2");
424 }
425
426 #[cfg(feature = "native")]
427 #[tokio::test]
428 async fn consume_returns_none_when_all_senders_dropped() {
429 let (tx, rx) = tokio::sync::mpsc::channel::<InboundMessage>(16);
430 let rx = tokio::sync::Mutex::new(rx);
431 tx.try_send(make_inbound("msg")).unwrap();
432 drop(tx);
433
434 let mut guard = rx.lock().await;
435 assert!(guard.recv().await.is_some());
436 assert!(guard.recv().await.is_none());
437 }
438
439 #[cfg(feature = "native")]
440 #[tokio::test]
441 async fn publish_inbound_error_on_closed_channel() {
442 let (tx, rx) = tokio::sync::mpsc::channel::<InboundMessage>(16);
443 drop(rx);
444
445 let result = tx.try_send(make_inbound("orphan"));
446 assert!(result.is_err());
447 }
448
449 #[cfg(feature = "native")]
450 #[tokio::test]
451 async fn dispatch_outbound_error_on_closed_channel() {
452 let (tx, rx) = tokio::sync::mpsc::channel::<OutboundMessage>(16);
453 drop(rx);
454
455 let result = tx.try_send(make_outbound("orphan"));
456 assert!(result.is_err());
457 }
458
459 #[cfg(feature = "native")]
460 #[tokio::test]
461 async fn concurrent_publish_and_consume() {
462 let bus = std::sync::Arc::new(MessageBus::new());
463 let bus_clone = bus.clone();
464
465 let producer = tokio::spawn(async move {
466 for i in 0..100 {
467 bus_clone
468 .publish_inbound_async(make_inbound(&format!("concurrent-{i}")))
469 .await
470 .unwrap();
471 }
472 });
473
474 let consumer = tokio::spawn(async move {
475 let mut received = Vec::new();
476 for _ in 0..100 {
477 if let Some(msg) = bus.consume_inbound().await {
478 received.push(msg.content);
479 }
480 }
481 received
482 });
483
484 producer.await.unwrap();
485 let results = consumer.await.unwrap();
486 assert_eq!(results.len(), 100);
487 }
488
489 #[cfg(feature = "native")]
490 #[test]
491 fn message_bus_is_send_sync() {
492 fn assert_send_sync<T: Send + Sync>() {}
493 assert_send_sync::<MessageBus>();
494 }
495
496 #[cfg(feature = "native")]
497 #[tokio::test]
498 async fn custom_capacity_bus() {
499 let bus = MessageBus::with_capacity(4);
500 for i in 0..4 {
501 bus.publish_inbound(make_inbound(&format!("msg-{i}")))
502 .unwrap();
503 }
504 let result = bus.publish_inbound(make_inbound("overflow"));
505 assert!(result.is_err());
506 let err_msg = result.unwrap_err().to_string();
507 assert!(
508 err_msg.contains("backpressure"),
509 "expected backpressure error, got: {err_msg}"
510 );
511 }
512
513 #[cfg(feature = "native")]
514 #[tokio::test]
515 async fn async_publish_waits_when_full() {
516 let bus = std::sync::Arc::new(MessageBus::with_capacity(2));
517 let bus_producer = bus.clone();
518 let bus_consumer = bus.clone();
519
520 bus.publish_inbound(make_inbound("a")).unwrap();
521 bus.publish_inbound(make_inbound("b")).unwrap();
522
523 let producer = tokio::spawn(async move {
524 bus_producer
525 .publish_inbound_async(make_inbound("c"))
526 .await
527 .unwrap();
528 });
529
530 tokio::task::yield_now().await;
531 let _ = bus_consumer.consume_inbound().await;
532
533 producer.await.unwrap();
534 let msg_b = bus_consumer.consume_inbound().await.unwrap();
535 let msg_c = bus_consumer.consume_inbound().await.unwrap();
536 assert_eq!(msg_b.content, "b");
537 assert_eq!(msg_c.content, "c");
538 }
539}