Skip to main content

mq_bridge/endpoints/memory/
endpoint.rs

1//  mq-bridge
2//  © Copyright 2025, by Marco Mengelkoch
3//  Licensed under MIT License, see License file for more details
4//  git clone https://github.com/marcomq/mq-bridge
5use super::memory_transport::MemoryTransport;
6use super::transport::{TransportChannel, TransportUrl};
7use crate::canonical_message::tracing_support::LazyMessageIds;
8use crate::event_store::{
9    event_store_exists, get_or_create_event_store, EventStore, EventStoreConsumer,
10};
11use crate::models::MemoryConfig;
12use crate::traits::{
13    BatchCommitFunc, BoxFuture, ConsumerError, EndpointStatus, MessageConsumer, MessageDisposition,
14    MessagePublisher, PublisherError, Received, ReceivedBatch, Sent, SentBatch,
15};
16use crate::CanonicalMessage;
17use anyhow::anyhow;
18use async_channel::{bounded, Receiver, Sender};
19use async_trait::async_trait;
20use once_cell::sync::Lazy;
21use std::any::Any;
22use std::collections::HashMap;
23use std::fmt;
24use std::sync::{Arc, Mutex};
25use tokio::sync::oneshot;
26use tracing::{info, trace, warn};
27
28#[cfg(unix)]
29use super::ipc_unix::UnixIpcTransport;
30#[cfg(windows)]
31use super::ipc_windows::WindowsIpcTransport;
32
33/// A map to hold memory channels for the duration of the bridge setup.
34/// This allows a consumer and publisher in different routes to connect to the same in-memory topic.
35static RUNTIME_MEMORY_CHANNELS: Lazy<Mutex<HashMap<String, MemoryChannel>>> =
36    Lazy::new(|| Mutex::new(HashMap::new()));
37
38/// A map to hold memory response channels.
39static RUNTIME_RESPONSE_CHANNELS: Lazy<Mutex<HashMap<String, MemoryResponseChannel>>> =
40    Lazy::new(|| Mutex::new(HashMap::new()));
41
42/// A shareable, thread-safe, in-memory channel for testing.
43///
44/// This struct holds the sender and receiver for an in-memory queue.
45/// It can be cloned and shared between your test code and the bridge's endpoints. It transports batches of messages.
46#[derive(Debug, Clone)]
47pub struct MemoryChannel {
48    pub sender: Sender<Vec<CanonicalMessage>>,
49    pub receiver: Receiver<Vec<CanonicalMessage>>,
50}
51
52impl MemoryChannel {
53    /// Creates a new batch channel with a specified capacity.
54    pub fn new(capacity: usize) -> Self {
55        let (sender, receiver) = bounded(capacity);
56        Self { sender, receiver }
57    }
58
59    /// Helper function for tests to easily send a message to the channel.
60    pub async fn send_message(&self, message: CanonicalMessage) -> anyhow::Result<()> {
61        self.sender.send(vec![message]).await?;
62        tracing::debug!("Message sent to memory {} channel", self.sender.len());
63        Ok(())
64    }
65
66    /// Helper function for tests to easily fill in messages.
67    pub async fn fill_messages(&self, messages: Vec<CanonicalMessage>) -> anyhow::Result<()> {
68        // Send the entire vector as a single batch.
69        self.sender
70            .send(messages)
71            .await
72            .map_err(|e| anyhow!("Memory channel was closed while filling messages: {}", e))?;
73        Ok(())
74    }
75
76    /// Closes the sender part of the channel.
77    pub fn close(&self) {
78        self.sender.close();
79    }
80
81    /// Helper function for tests to drain all messages from the channel.
82    pub fn drain_messages(&self) -> Vec<CanonicalMessage> {
83        let mut messages = Vec::new();
84        // Drain all batches from the channel and flatten them into a single Vec.
85        while let Ok(batch) = self.receiver.try_recv() {
86            messages.extend(batch);
87        }
88        messages
89    }
90
91    /// Returns the number of bulk messages in the channel.
92    pub fn len(&self) -> usize {
93        self.receiver.len()
94    }
95
96    /// Returns the number of messages currently in the channel.
97    pub fn is_empty(&self) -> bool {
98        self.receiver.is_empty()
99    }
100}
101
102/// A shareable, thread-safe, in-memory channel for responses.
103#[derive(Debug, Clone)]
104pub struct MemoryResponseChannel {
105    pub sender: Sender<CanonicalMessage>,
106    pub receiver: Receiver<CanonicalMessage>,
107    waiters: Arc<tokio::sync::Mutex<HashMap<String, oneshot::Sender<CanonicalMessage>>>>,
108}
109
110impl MemoryResponseChannel {
111    pub fn new(capacity: usize) -> Self {
112        let (sender, receiver) = bounded(capacity);
113        Self {
114            sender,
115            receiver,
116            waiters: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
117        }
118    }
119
120    pub fn close(&self) {
121        self.sender.close();
122    }
123
124    pub fn len(&self) -> usize {
125        self.receiver.len()
126    }
127
128    pub fn is_empty(&self) -> bool {
129        self.receiver.is_empty()
130    }
131
132    pub async fn wait_for_response(&self) -> anyhow::Result<CanonicalMessage> {
133        self.receiver
134            .recv()
135            .await
136            .map_err(|e| anyhow!("Error receiving response: {}", e))
137    }
138
139    pub async fn register_waiter(
140        &self,
141        correlation_id: &str,
142        sender: oneshot::Sender<CanonicalMessage>,
143    ) -> anyhow::Result<()> {
144        let mut waiters = self.waiters.lock().await;
145        if waiters.contains_key(correlation_id) {
146            return Err(anyhow!(
147                "Correlation ID {} already registered",
148                correlation_id
149            ));
150        }
151        waiters.insert(correlation_id.to_string(), sender);
152        Ok(())
153    }
154
155    pub async fn remove_waiter(
156        &self,
157        correlation_id: &str,
158    ) -> Option<oneshot::Sender<CanonicalMessage>> {
159        self.waiters.lock().await.remove(correlation_id)
160    }
161}
162
163/// Gets a shared `MemoryChannel` for a given topic, creating it if it doesn't exist.
164pub fn get_or_create_channel(config: &MemoryConfig) -> MemoryChannel {
165    let topic = memory_namespace(config).unwrap_or_else(|_| config.topic.clone());
166    let mut channels = RUNTIME_MEMORY_CHANNELS.lock().unwrap();
167    channels
168        .entry(topic.clone()) // Use the HashMap's entry API
169        .or_insert_with(|| {
170            info!(topic = %topic, "Creating new runtime memory channel");
171            MemoryChannel::new(config.capacity.unwrap_or(100))
172        })
173        .clone()
174}
175
176/// Gets a shared `MemoryResponseChannel` for a given topic, creating it if it doesn't exist.
177pub fn get_or_create_response_channel(topic: &str) -> MemoryResponseChannel {
178    let mut channels = RUNTIME_RESPONSE_CHANNELS.lock().unwrap();
179    channels
180        .entry(topic.to_string())
181        .or_insert_with(|| {
182            info!(topic = %topic, "Creating new runtime memory response channel");
183            MemoryResponseChannel::new(100)
184        })
185        .clone()
186}
187
188fn memory_channel_exists(topic: &str) -> bool {
189    let channels = RUNTIME_MEMORY_CHANNELS.lock().unwrap();
190    channels.contains_key(topic)
191}
192
193fn resolved_transport(config: &MemoryConfig) -> anyhow::Result<TransportUrl> {
194    let identifier = config.get_transport_identifier()?;
195    TransportUrl::parse(&identifier)
196}
197
198fn memory_namespace(config: &MemoryConfig) -> anyhow::Result<String> {
199    match resolved_transport(config)? {
200        TransportUrl::Memory { namespace } => Ok(namespace),
201        other => Err(anyhow!(
202            "MemoryConfig uses IPC transport '{}', which requires async endpoint construction",
203            other.display_name()
204        )),
205    }
206}
207
208fn normalized_memory_config(config: &MemoryConfig) -> anyhow::Result<MemoryConfig> {
209    let mut normalized = config.clone();
210    normalized.topic = memory_namespace(config)?;
211    normalized.url = None;
212    Ok(normalized.with_smart_defaults())
213}
214
215/// Create a transport based on the URL scheme
216#[allow(dead_code)]
217async fn create_transport_from_url(
218    url: &TransportUrl,
219    capacity: usize,
220    is_server: bool,
221) -> anyhow::Result<Arc<dyn TransportChannel>> {
222    match url {
223        TransportUrl::Memory { namespace } => {
224            info!(namespace = %namespace, "Creating in-process memory transport");
225            Ok(Arc::new(MemoryTransport::new(capacity)))
226        }
227        #[cfg(unix)]
228        TransportUrl::Unix { path } => {
229            if is_server {
230                info!(path = %path, "Creating Unix IPC server transport");
231                let transport = UnixIpcTransport::new_server(path, capacity).await?;
232                Ok(Arc::new(transport))
233            } else {
234                info!(path = %path, "Creating Unix IPC client transport");
235                let transport = UnixIpcTransport::new_client(path, capacity).await?;
236                Ok(Arc::new(transport))
237            }
238        }
239        #[cfg(windows)]
240        TransportUrl::Pipe { name } => {
241            if is_server {
242                info!(pipe = %name, "Creating Windows Named Pipe server transport");
243                let transport = WindowsIpcTransport::new_server(name, capacity).await?;
244                Ok(Arc::new(transport))
245            } else {
246                info!(pipe = %name, "Creating Windows Named Pipe client transport");
247                let transport = WindowsIpcTransport::new_client(name, capacity).await?;
248                Ok(Arc::new(transport))
249            }
250        }
251        #[cfg(not(any(unix, windows)))]
252        _ => Err(anyhow!("IPC transport not supported on this platform")),
253    }
254}
255
256/// A sink that sends messages to an in-memory channel.
257#[derive(Debug, Clone)]
258pub struct MemoryPublisher {
259    topic: String,
260    backend: PublisherBackend,
261    request_reply: bool,
262    request_timeout: std::time::Duration,
263}
264
265#[derive(Clone)]
266enum PublisherBackend {
267    Queue(Sender<Vec<CanonicalMessage>>),
268    Log(Arc<EventStore>),
269    Transport(Arc<dyn TransportChannel>),
270}
271
272impl fmt::Debug for PublisherBackend {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        match self {
275            Self::Queue(_) => f.write_str("Queue(..)"),
276            Self::Log(_) => f.write_str("Log(..)"),
277            Self::Transport(_) => f.write_str("Transport(..)"),
278        }
279    }
280}
281
282impl MemoryPublisher {
283    pub fn new(config: &MemoryConfig) -> anyhow::Result<Self> {
284        let config = normalized_memory_config(config)?;
285        let channel_exists = memory_channel_exists(&config.topic);
286        let store_exists = event_store_exists(&config.topic);
287
288        let backend = if config.subscribe_mode {
289            if channel_exists {
290                return Err(anyhow!("Topic '{}' is already active as a Queue (MemoryChannel), but Subscriber mode (EventStore) was requested.", config.topic));
291            }
292            let store = get_or_create_event_store(&config.topic);
293            PublisherBackend::Log(store)
294        } else if store_exists {
295            // Adaptive behavior: If an EventStore already exists, we publish to it even if
296            // subscribe_mode wasn't explicitly set. This prevents split-brain scenarios.
297            tracing::debug!(topic = %config.topic, "Adapting publisher to Log mode due to existing EventStore");
298            let store = get_or_create_event_store(&config.topic);
299            PublisherBackend::Log(store)
300        } else {
301            let channel = get_or_create_channel(&config);
302            PublisherBackend::Queue(channel.sender)
303        };
304
305        Ok(Self {
306            topic: config.topic.clone(),
307            backend,
308            request_reply: config.request_reply,
309            request_timeout: std::time::Duration::from_millis(
310                config.request_timeout_ms.unwrap_or(30000),
311            ),
312        })
313    }
314
315    pub async fn new_async(config: &MemoryConfig) -> anyhow::Result<Self> {
316        let url = resolved_transport(config)?;
317        match &url {
318            TransportUrl::Memory { .. } => Self::new(config),
319            _ => {
320                if config.subscribe_mode {
321                    return Err(anyhow!(
322                        "IPC memory publishers do not support subscribe_mode"
323                    ));
324                }
325                if config.request_reply {
326                    return Err(anyhow!(
327                        "IPC memory publishers do not yet support request_reply"
328                    ));
329                }
330                let capacity = config.capacity.unwrap_or(100);
331                let transport = create_transport_from_url(&url, capacity, false).await?;
332                Ok(Self {
333                    topic: url.display_name(),
334                    backend: PublisherBackend::Transport(transport),
335                    request_reply: false,
336                    request_timeout: std::time::Duration::from_millis(
337                        config.request_timeout_ms.unwrap_or(30000),
338                    ),
339                })
340            }
341        }
342    }
343
344    /// Creates a new local memory publisher.
345    ///
346    /// This method creates a new in-memory publisher with the specified topic and capacity.
347    /// The publisher will send messages to the in-memory channel for the specified topic.
348    pub fn new_local(topic: &str, capacity: usize) -> Self {
349        Self::new(&MemoryConfig {
350            topic: topic.to_string(),
351            capacity: Some(capacity),
352            ..Default::default()
353        })
354        .expect("Failed to create local memory publisher")
355    }
356
357    /// Note: This helper is primarily for tests expecting a Queue.    
358    /// If used on a broadcast publisher, it will create a separate Queue channel.
359    pub fn channel(&self) -> MemoryChannel {
360        get_or_create_channel(&MemoryConfig {
361            topic: self.topic.clone(),
362            capacity: None,
363            ..Default::default()
364        })
365    }
366}
367
368#[async_trait]
369impl MessagePublisher for MemoryPublisher {
370    async fn send(&self, mut message: CanonicalMessage) -> Result<Sent, PublisherError> {
371        match &self.backend {
372            PublisherBackend::Log(store) => {
373                store.append(message).await;
374                Ok(Sent::Ack)
375            }
376            PublisherBackend::Queue(sender) => {
377                if self.request_reply {
378                    let cid = message
379                        .metadata
380                        .entry("correlation_id".to_string())
381                        .or_insert_with(fast_uuid_v7::gen_id_string)
382                        .clone();
383
384                    let (tx, rx) = oneshot::channel();
385
386                    // Register waiter before sending
387                    let response_channel = get_or_create_response_channel(&self.topic);
388                    response_channel
389                        .register_waiter(&cid, tx)
390                        .await
391                        .map_err(PublisherError::NonRetryable)?;
392
393                    // Send the message
394                    // We use the internal sender directly to avoid recursion or cloning issues
395                    if let Err(e) = sender.send(vec![message]).await {
396                        response_channel.remove_waiter(&cid).await;
397                        return Err(anyhow!("Failed to send to memory channel: {}", e).into());
398                    }
399
400                    // Wait for the response
401                    let response = match tokio::time::timeout(self.request_timeout, rx).await {
402                        Ok(Ok(resp)) => resp,
403                        Ok(Err(e)) => {
404                            response_channel.remove_waiter(&cid).await;
405                            return Err(anyhow!(
406                                "Failed to receive response for correlation_id {}: {}",
407                                cid,
408                                e
409                            )
410                            .into());
411                        }
412                        Err(_) => {
413                            response_channel.remove_waiter(&cid).await;
414                            return Err(PublisherError::Retryable(anyhow!(
415                                "Request timed out waiting for response for correlation_id {}",
416                                cid
417                            )));
418                        }
419                    };
420
421                    Ok(Sent::Response(response))
422                } else {
423                    sender
424                        .send(vec![message])
425                        .await
426                        .map_err(|e| anyhow!("Failed to send to memory channel: {}", e))?;
427                    Ok(Sent::Ack)
428                }
429            }
430            PublisherBackend::Transport(transport) => {
431                transport
432                    .send_batch(vec![message])
433                    .await
434                    .map_err(|e| anyhow!("Failed to send via memory transport: {}", e))?;
435                Ok(Sent::Ack)
436            }
437        }
438    }
439
440    async fn send_batch(
441        &self,
442        messages: Vec<CanonicalMessage>,
443    ) -> Result<SentBatch, PublisherError> {
444        match &self.backend {
445            PublisherBackend::Log(store) => {
446                trace!(
447                    topic = %self.topic,
448                    message_ids = ?LazyMessageIds(&messages),
449                    "Appending batch to event store"
450                );
451                store.append_batch(messages).await;
452                Ok(SentBatch::Ack)
453            }
454            PublisherBackend::Queue(sender) => {
455                trace!(
456                    topic = %self.topic,
457                    message_ids = ?LazyMessageIds(&messages),
458                    "Sending batch to memory channel. Current batch count: {}",
459                    sender.len()
460                );
461                sender
462                    .send(messages)
463                    .await
464                    .map_err(|e| anyhow!("Failed to send to memory channel: {}", e))?;
465                Ok(SentBatch::Ack)
466            }
467            PublisherBackend::Transport(transport) => {
468                trace!(
469                    topic = %self.topic,
470                    message_ids = ?LazyMessageIds(&messages),
471                    "Sending batch to memory transport"
472                );
473                transport
474                    .send_batch(messages)
475                    .await
476                    .map_err(|e| anyhow!("Failed to send batch via memory transport: {}", e))?;
477                Ok(SentBatch::Ack)
478            }
479        }
480    }
481
482    async fn status(&self) -> EndpointStatus {
483        match &self.backend {
484            PublisherBackend::Queue(sender) => EndpointStatus {
485                healthy: !sender.is_closed(),
486                target: self.topic.clone(),
487                pending: Some(sender.len()),
488                capacity: Some(sender.capacity().unwrap_or(0)),
489                ..Default::default()
490            },
491            PublisherBackend::Log(_store) => EndpointStatus {
492                healthy: true,
493                target: self.topic.clone(),
494                details: serde_json::json!({
495                    "mode": "event_store"
496                }),
497                ..Default::default()
498            },
499            PublisherBackend::Transport(transport) => EndpointStatus {
500                healthy: !transport.is_closed(),
501                target: self.topic.clone(),
502                pending: Some(transport.len()),
503                capacity: transport.capacity(),
504                details: serde_json::json!({
505                    "mode": "transport"
506                }),
507                ..Default::default()
508            },
509        }
510    }
511
512    fn as_any(&self) -> &dyn Any {
513        self
514    }
515}
516
517/// A queue-based consumer (legacy behavior).
518#[derive(Debug)]
519pub struct MemoryQueueConsumer {
520    topic: String,
521    receiver: Receiver<Vec<CanonicalMessage>>,
522    // Internal buffer to hold messages from a received batch.
523    buffer: Vec<CanonicalMessage>,
524    enable_nack: bool,
525}
526
527#[derive(Clone)]
528pub struct TransportQueueConsumer {
529    topic: String,
530    transport: Arc<dyn TransportChannel>,
531    buffer: Vec<CanonicalMessage>,
532    enable_nack: bool,
533}
534
535impl fmt::Debug for TransportQueueConsumer {
536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537        f.debug_struct("TransportQueueConsumer")
538            .field("topic", &self.topic)
539            .field("enable_nack", &self.enable_nack)
540            .finish_non_exhaustive()
541    }
542}
543
544/// A source that reads messages from an in-memory channel or event store.
545#[derive(Debug)]
546pub enum MemoryConsumer {
547    Queue(MemoryQueueConsumer),
548    Transport(TransportQueueConsumer),
549    Log {
550        consumer: EventStoreConsumer,
551        topic: String,
552    },
553}
554
555impl MemoryConsumer {
556    pub fn new(config: &MemoryConfig) -> anyhow::Result<Self> {
557        let config = normalized_memory_config(config)?;
558        let channel_exists = memory_channel_exists(&config.topic);
559        let store_exists = event_store_exists(&config.topic);
560
561        if config.subscribe_mode {
562            if channel_exists {
563                return Err(anyhow!("Topic '{}' is already active as a Queue (MemoryChannel), but Subscriber mode (EventStore) was requested.", config.topic));
564            }
565            let store = get_or_create_event_store(&config.topic);
566            // For subscriber mode, we generate a unique ID if one isn't implicit in the usage.
567            // However, MemorySubscriber struct usually handles the ID.
568            // If MemoryConsumer is used directly with subscribe_mode=true, we assume a default ID or ephemeral.
569            let subscriber_id = format!("{}-consumer", config.topic);
570            info!(topic = %config.topic, subscriber_id = %subscriber_id, "Memory consumer (Log mode) connected");
571            let consumer = store.consumer(subscriber_id);
572            Ok(Self::Log {
573                consumer,
574                topic: config.topic.clone(),
575            })
576        } else {
577            if store_exists {
578                // Unlike the Publisher, we cannot silently adapt to Log mode here.
579                // The EventStore implementation currently supports Pub/Sub (broadcast) only.
580                // Adapting would result in this consumer receiving all messages, violating
581                // the expected Queue (competing consumer) semantics requested by `subscribe_mode: false`.
582                return Err(anyhow!("Topic '{}' is already active as a Subscriber Log (EventStore), but Queue mode (MemoryChannel) was requested.", config.topic));
583            }
584            let queue = MemoryQueueConsumer::new(&config)?;
585            Ok(Self::Queue(queue))
586        }
587    }
588
589    pub async fn new_async(config: &MemoryConfig) -> anyhow::Result<Self> {
590        let url = resolved_transport(config)?;
591        match &url {
592            TransportUrl::Memory { .. } => Self::new(config),
593            _ => {
594                if config.subscribe_mode {
595                    return Err(anyhow!(
596                        "IPC memory consumers do not support subscribe_mode"
597                    ));
598                }
599                let config = config.clone().with_smart_defaults();
600                let capacity = config.capacity.unwrap_or(100);
601                let transport = create_transport_from_url(&url, capacity, true).await?;
602                Ok(Self::Transport(TransportQueueConsumer {
603                    topic: url.display_name(),
604                    transport,
605                    buffer: Vec::new(),
606                    enable_nack: config.enable_nack,
607                }))
608            }
609        }
610    }
611}
612
613impl Drop for MemoryQueueConsumer {
614    fn drop(&mut self) {
615        if !self.buffer.is_empty() {
616            let mut messages = std::mem::take(&mut self.buffer);
617            messages.reverse();
618
619            let channel = get_or_create_channel(&MemoryConfig {
620                topic: self.topic.clone(),
621                capacity: None,
622                ..Default::default()
623            });
624
625            match channel.sender.try_send(messages) {
626                Ok(_) => {
627                    info!(topic = %self.topic, "Requeued buffered messages on consumer drop");
628                }
629                Err(e) => {
630                    let msgs = match e {
631                        async_channel::TrySendError::Full(m) => m,
632                        async_channel::TrySendError::Closed(m) => m,
633                    };
634                    warn!(topic = %self.topic, "Channel full on drop, spawning async requeue");
635                    let sender = channel.sender.clone();
636                    if let Ok(handle) = tokio::runtime::Handle::try_current() {
637                        handle.spawn(async move {
638                            if let Err(e) = sender.send(msgs).await {
639                                tracing::error!(
640                                    "Failed to requeue buffered messages in background: {}",
641                                    e
642                                );
643                            }
644                        });
645                    } else {
646                        tracing::error!(topic = %self.topic, "No active runtime found, could not requeue buffered messages on consumer drop");
647                    }
648                }
649            }
650        }
651    }
652}
653
654impl MemoryQueueConsumer {
655    pub fn new(config: &MemoryConfig) -> anyhow::Result<Self> {
656        let channel = get_or_create_channel(config);
657        let buffer = if let Some(capacity) = config.capacity {
658            Vec::with_capacity(capacity)
659        } else {
660            Vec::new()
661        };
662        Ok(Self {
663            topic: config.topic.clone(),
664            receiver: channel.receiver.clone(),
665            buffer,
666            enable_nack: config.enable_nack,
667        })
668    }
669
670    async fn get_buffered_msgs(
671        &mut self,
672        max_messages: usize,
673    ) -> Result<Vec<CanonicalMessage>, ConsumerError> {
674        // If the internal buffer has messages, return them first.
675        if self.buffer.is_empty() {
676            // Buffer is empty. Wait for a new batch from the channel.
677            self.buffer = match self.receiver.recv().await {
678                Ok(batch) => batch,
679                Err(_) => return Err(ConsumerError::EndOfStream),
680            };
681            // Reverse the buffer so we can efficiently pop from the end.
682            self.buffer.reverse();
683        }
684
685        // Determine the number of messages to take from the buffer.
686        let num_to_take = self.buffer.len().min(max_messages);
687        let split_at = self.buffer.len() - num_to_take;
688
689        // `split_off` is highly efficient. It splits the Vec in two at the given
690        // index and returns the part after the index, leaving the first part.
691        let mut messages = self.buffer.split_off(split_at);
692        messages.reverse(); // Reverse back to original order.
693        Ok(messages)
694    }
695}
696
697struct RequeueGuard {
698    topic: String,
699    messages: Vec<CanonicalMessage>,
700}
701
702impl Drop for RequeueGuard {
703    fn drop(&mut self) {
704        if !self.messages.is_empty() {
705            let topic = self.topic.clone();
706            let count = self.messages.len();
707            let messages = std::mem::take(&mut self.messages);
708
709            let channel = get_or_create_channel(&MemoryConfig {
710                topic: topic.clone(),
711                capacity: None,
712                ..Default::default()
713            });
714
715            match channel.sender.try_send(messages) {
716                Ok(_) => {
717                    tracing::info!(topic = %topic, count, "Requeued dropped batch via RequeueGuard");
718                }
719                Err(e) => {
720                    let msgs = match e {
721                        async_channel::TrySendError::Full(m) => m,
722                        async_channel::TrySendError::Closed(m) => m,
723                    };
724                    tracing::warn!(topic = %topic, count, "Failed to requeue dropped batch (channel full/closed), spawning retry");
725                    let sender = channel.sender.clone();
726                    if let Ok(handle) = tokio::runtime::Handle::try_current() {
727                        handle.spawn(async move {
728                            if let Err(e) = sender.send(msgs).await {
729                                tracing::error!(
730                                    "Failed to requeue dropped batch in background: {}",
731                                    e
732                                );
733                            }
734                        });
735                    } else {
736                        tracing::error!(topic = %topic, count, "No active runtime found, could not requeue dropped batch via RequeueGuard");
737                    }
738                }
739            }
740        }
741    }
742}
743
744#[async_trait]
745impl MessageConsumer for MemoryQueueConsumer {
746    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
747        // If the internal buffer has messages, return them first.
748
749        let mut messages = self.get_buffered_msgs(max_messages).await?;
750        while messages.len() < max_messages / 2 {
751            if let Ok(mut next_batch) = self.receiver.try_recv() {
752                if next_batch.len() + messages.len() > max_messages {
753                    let needed = max_messages - messages.len();
754                    let mut to_buffer = next_batch.split_off(needed);
755                    messages.append(&mut next_batch);
756                    self.buffer.append(&mut to_buffer);
757                    self.buffer.reverse();
758                    break;
759                } else {
760                    messages.append(&mut next_batch);
761                }
762            } else {
763                break;
764            }
765        }
766        trace!(count = messages.len(), topic = %self.topic, message_ids = ?LazyMessageIds(&messages), "Received batch of memory messages");
767        if messages.is_empty() {
768            return Ok(ReceivedBatch {
769                messages: Vec::new(),
770                commit: Box::new(|_| {
771                    Box::pin(async move { Ok(()) }) as BoxFuture<'static, anyhow::Result<()>>
772                }),
773            });
774        }
775
776        let topic = self.topic.clone();
777        let expected_count = messages.len();
778        let correlation_ids: Vec<Option<String>> = messages
779            .iter()
780            .map(|m| m.metadata.get("correlation_id").cloned())
781            .collect();
782
783        // Guard to requeue messages if the batch is dropped without commit/nack.
784        let mut guard = if self.enable_nack {
785            Some(RequeueGuard {
786                topic: self.topic.clone(),
787                messages: messages.clone(),
788            })
789        } else {
790            None
791        };
792
793        let commit = Box::new(move |dispositions: Vec<MessageDisposition>| {
794            Box::pin(async move {
795                if dispositions.len() != expected_count {
796                    return Err(anyhow::anyhow!(
797                        "Memory batch commit received mismatched disposition count: expected {}, got {}",
798                        expected_count,
799                        dispositions.len()
800                    ));
801                }
802
803                // Clone messages from guard to keep it armed during async operations
804                let messages_for_retry = if let Some(g) = &guard {
805                    g.messages.clone()
806                } else {
807                    Vec::new()
808                };
809
810                let response_channel = get_or_create_response_channel(&topic);
811                let mut to_requeue = Vec::new();
812
813                for (i, disposition) in dispositions.into_iter().enumerate() {
814                    match disposition {
815                        MessageDisposition::Reply(resp) => {
816                            handle_memory_reply(resp, i, &correlation_ids, &response_channel).await;
817                        }
818                        MessageDisposition::Nack => {
819                            if let Some(msg) = messages_for_retry.get(i) {
820                                warn!("Requeueing nacked message {}", i);
821                                to_requeue.push(msg.clone());
822                            } else {
823                                warn!("Nack for index {} but no message in retry buffer!", i);
824                            }
825                        }
826                        MessageDisposition::Ack => {}
827                    }
828                }
829
830                if !to_requeue.is_empty() {
831                    let main_channel = get_or_create_channel(&MemoryConfig {
832                        topic: topic.to_string(),
833                        capacity: None,
834                        ..Default::default()
835                    });
836                    if main_channel.sender.send(to_requeue).await.is_err() {
837                        tracing::error!("Failed to re-queue NACKed messages to memory channel as it was closed.");
838                    }
839                }
840
841                // Disarm the guard after all awaits are finished.
842                if let Some(g) = &mut guard {
843                    std::mem::take(&mut g.messages);
844                }
845
846                Ok(())
847            }) as BoxFuture<'static, anyhow::Result<()>>
848        }) as BatchCommitFunc;
849        Ok(ReceivedBatch { messages, commit })
850    }
851
852    async fn status(&self) -> EndpointStatus {
853        let pending = self.receiver.len();
854        let capacity = self.receiver.capacity().unwrap_or(0);
855        EndpointStatus {
856            healthy: !self.receiver.is_closed(),
857            target: self.topic.clone(),
858            pending: Some(pending),
859            capacity: Some(capacity),
860            ..Default::default()
861        }
862    }
863
864    fn as_any(&self) -> &dyn Any {
865        self
866    }
867}
868
869#[async_trait]
870impl MessageConsumer for TransportQueueConsumer {
871    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
872        let mut messages = Vec::with_capacity(max_messages);
873        let buffered = self.buffer.len().min(max_messages);
874        if buffered > 0 {
875            messages.extend(self.buffer.drain(..buffered));
876        }
877
878        if messages.len() < max_messages {
879            let mut received = self.transport.recv_batch().await.map_err(|e| {
880                ConsumerError::Connection(anyhow!("Failed to receive via memory transport: {}", e))
881            })?;
882            messages.append(&mut received);
883            if messages.len() > max_messages {
884                self.buffer = messages.split_off(max_messages);
885            }
886        }
887
888        trace!(count = messages.len(), topic = %self.topic, message_ids = ?LazyMessageIds(&messages), "Received batch from memory transport");
889
890        let topic = self.topic.clone();
891        let transport = self.transport.clone();
892        let enable_nack = self.enable_nack;
893        let expected_count = messages.len();
894        let messages_for_retry = if enable_nack {
895            messages.clone()
896        } else {
897            Vec::new()
898        };
899
900        let commit = Box::new(move |dispositions: Vec<MessageDisposition>| {
901            let transport = transport.clone();
902            let topic = topic.clone();
903            let messages_for_retry = messages_for_retry.clone();
904            Box::pin(async move {
905                if dispositions.len() != expected_count {
906                    return Err(anyhow::anyhow!(
907                        "Memory transport batch commit received mismatched disposition count: expected {}, got {}",
908                        expected_count,
909                        dispositions.len()
910                    ));
911                }
912
913                let mut to_requeue = Vec::new();
914                for (i, disposition) in dispositions.into_iter().enumerate() {
915                    match disposition {
916                        MessageDisposition::Nack if enable_nack => {
917                            if let Some(msg) = messages_for_retry.get(i) {
918                                to_requeue.push(msg.clone());
919                            }
920                        }
921                        MessageDisposition::Reply(_) => {
922                            tracing::warn!(topic = %topic, "IPC memory transport does not support reply dispositions");
923                        }
924                        MessageDisposition::Ack | MessageDisposition::Nack => {}
925                    }
926                }
927
928                if !to_requeue.is_empty() {
929                    transport.send_batch(to_requeue).await?;
930                }
931
932                Ok(())
933            }) as BoxFuture<'static, anyhow::Result<()>>
934        }) as BatchCommitFunc;
935
936        Ok(ReceivedBatch { messages, commit })
937    }
938
939    async fn status(&self) -> EndpointStatus {
940        EndpointStatus {
941            healthy: !self.transport.is_closed(),
942            target: self.topic.clone(),
943            pending: Some(self.transport.len()),
944            capacity: self.transport.capacity(),
945            details: serde_json::json!({
946                "mode": "transport"
947            }),
948            ..Default::default()
949        }
950    }
951
952    fn as_any(&self) -> &dyn Any {
953        self
954    }
955}
956
957async fn handle_memory_reply(
958    mut resp: CanonicalMessage,
959    index: usize,
960    correlation_ids: &[Option<String>],
961    response_channel: &MemoryResponseChannel,
962) {
963    if !resp.metadata.contains_key("correlation_id") {
964        if let Some(Some(cid)) = correlation_ids.get(index) {
965            resp.metadata
966                .insert("correlation_id".to_string(), cid.clone());
967        }
968    }
969
970    if let Some(cid) = resp.metadata.get("correlation_id") {
971        if let Some(tx) = response_channel.remove_waiter(cid).await {
972            let _ = tx.send(resp);
973            return;
974        }
975    }
976    let _ = response_channel.sender.send(resp).await;
977}
978
979#[async_trait]
980impl MessageConsumer for MemoryConsumer {
981    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
982        match self {
983            Self::Queue(q) => q.receive_batch(max_messages).await,
984            Self::Transport(t) => t.receive_batch(max_messages).await,
985            Self::Log { consumer, .. } => consumer.receive_batch(max_messages).await,
986        }
987    }
988
989    async fn status(&self) -> EndpointStatus {
990        match self {
991            Self::Queue(q) => q.status().await,
992            Self::Transport(t) => t.status().await,
993            Self::Log { consumer, .. } => consumer.status().await,
994        }
995    }
996
997    fn as_any(&self) -> &dyn Any {
998        self
999    }
1000}
1001
1002impl MemoryConsumer {
1003    pub fn new_local(topic: &str, capacity: usize) -> Self {
1004        Self::new(&MemoryConfig {
1005            topic: topic.to_string(),
1006            capacity: Some(capacity),
1007            ..Default::default()
1008        })
1009        .expect("Failed to create local memory consumer")
1010    }
1011    pub fn channel(&self) -> MemoryChannel {
1012        let topic = match self {
1013            Self::Queue(q) => &q.topic,
1014            Self::Transport(t) => &t.topic,
1015            Self::Log { topic, .. } => topic,
1016        };
1017        get_or_create_channel(&MemoryConfig {
1018            topic: topic.clone(),
1019            ..Default::default()
1020        })
1021    }
1022}
1023
1024pub struct MemorySubscriber {
1025    consumer: MemoryConsumer,
1026}
1027
1028impl MemorySubscriber {
1029    pub fn new(config: &MemoryConfig, id: &str) -> anyhow::Result<Self> {
1030        let mut sub_config = config.clone();
1031        // If subscribe_mode is true, we use EventStore with the original topic but unique subscriber ID.
1032        // If false (legacy), we use the suffixed topic queue.
1033        let consumer = if config.subscribe_mode {
1034            let store = get_or_create_event_store(&config.topic);
1035            MemoryConsumer::Log {
1036                consumer: store.consumer(id.to_string()),
1037                topic: config.topic.clone(),
1038            }
1039        } else {
1040            sub_config.topic = format!("{}-{}", config.topic, id);
1041            MemoryConsumer::new(&sub_config)?
1042        };
1043        Ok(Self { consumer })
1044    }
1045}
1046
1047#[async_trait]
1048impl MessageConsumer for MemorySubscriber {
1049    async fn receive_batch(&mut self, max_messages: usize) -> Result<ReceivedBatch, ConsumerError> {
1050        self.consumer.receive_batch(max_messages).await
1051    }
1052
1053    async fn receive(&mut self) -> Result<Received, ConsumerError> {
1054        self.consumer.receive().await
1055    }
1056
1057    fn as_any(&self) -> &dyn Any {
1058        self
1059    }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065    use crate::models::{Endpoint, Route};
1066    use crate::traits::Handled;
1067    use crate::{msg, CanonicalMessage};
1068    use serde_json::json;
1069    use tokio::time::sleep;
1070
1071    #[tokio::test]
1072    async fn test_memory_channel_integration() {
1073        let mut consumer = MemoryConsumer::new_local("test-mem1", 10);
1074        let publisher = MemoryPublisher::new_local("test-mem1", 10);
1075
1076        let msg = msg!(json!({"hello": "memory"}));
1077
1078        // Send a message via the publisher
1079        publisher.send(msg.clone()).await.unwrap();
1080
1081        sleep(std::time::Duration::from_millis(10)).await;
1082        // Receive it with the consumer
1083        let received = consumer.receive().await.unwrap();
1084        let _ = (received.commit)(MessageDisposition::Ack).await;
1085        assert_eq!(received.message.payload, msg.payload);
1086        assert_eq!(consumer.channel().len(), 0);
1087    }
1088
1089    #[tokio::test]
1090    async fn test_memory_url_alias_uses_same_channel_as_legacy_topic() {
1091        let mut consumer = MemoryConsumer::new(&MemoryConfig::new("test-memory-url", Some(10)))
1092            .expect("legacy topic consumer should be in-process memory");
1093        let publisher = MemoryPublisher::new_async(&MemoryConfig::new_with_url(
1094            "memory://test-memory-url",
1095            Some(10),
1096        ))
1097        .await
1098        .expect("memory URL publisher should be in-process memory");
1099
1100        let msg = msg!(json!({"hello": "memory-url"}));
1101        publisher.send(msg.clone()).await.unwrap();
1102
1103        let received = consumer.receive().await.unwrap();
1104        let _ = (received.commit)(MessageDisposition::Ack).await;
1105        assert_eq!(received.message.payload, msg.payload);
1106    }
1107
1108    #[cfg(unix)]
1109    #[tokio::test]
1110    async fn test_unix_ipc_endpoint_constructors_roundtrip() {
1111        let temp_dir = tempfile::TempDir::new().unwrap();
1112        let socket_path = temp_dir.path().join("endpoint.sock");
1113        let url = format!("unix://{}", socket_path.display());
1114        let config = MemoryConfig::new_with_url(url, Some(10));
1115
1116        assert!(config.clone().with_smart_defaults().enable_nack);
1117
1118        let mut consumer = MemoryConsumer::new_async(&config)
1119            .await
1120            .expect("IPC consumer should create a Unix socket server");
1121        let publisher = MemoryPublisher::new_async(&config)
1122            .await
1123            .expect("IPC publisher should connect to the Unix socket server");
1124
1125        let msg = CanonicalMessage::from_vec(b"endpoint-ipc");
1126        publisher.send(msg.clone()).await.unwrap();
1127
1128        let received = consumer.receive().await.unwrap();
1129        (received.commit)(MessageDisposition::Ack).await.unwrap();
1130        assert_eq!(received.message.payload.as_ref(), b"endpoint-ipc");
1131    }
1132
1133    #[tokio::test]
1134    async fn test_memory_publisher_and_consumer_integration() {
1135        let mut consumer = MemoryConsumer::new_local("test-mem2", 10);
1136        let publisher = MemoryPublisher::new_local("test-mem2", 10);
1137
1138        let msg1 = msg!(json!({"message": "one"}));
1139        let msg2 = msg!(json!({"message": "two"}));
1140        let msg3 = msg!(json!({"message": "three"}));
1141
1142        // 3. Send messages via the publisher
1143        publisher
1144            .send_batch(vec![msg1.clone(), msg2.clone()])
1145            .await
1146            .unwrap();
1147        publisher.send(msg3.clone()).await.unwrap();
1148
1149        // 4. Verify the channel has the messages
1150        assert_eq!(publisher.channel().len(), 2);
1151
1152        // 5. Receive the messages and verify them
1153        let received1 = consumer.receive().await.unwrap();
1154        let _ = (received1.commit)(MessageDisposition::Ack).await;
1155        assert_eq!(received1.message.payload, msg1.payload);
1156
1157        let batch2 = consumer.receive_batch(1).await.unwrap();
1158        let (received_msg2, commit2) = (batch2.messages, batch2.commit);
1159        let _ = commit2(vec![MessageDisposition::Ack; received_msg2.len()]).await;
1160        assert_eq!(received_msg2.len(), 1);
1161        assert_eq!(received_msg2.first().unwrap().payload, msg2.payload);
1162        let batch3 = consumer.receive_batch(2).await.unwrap();
1163        let (received_msg3, commit3) = (batch3.messages, batch3.commit);
1164        let _ = commit3(vec![MessageDisposition::Ack; received_msg3.len()]).await;
1165        assert_eq!(received_msg3.first().unwrap().payload, msg3.payload);
1166
1167        // 6. Verify that the channel is now empty
1168        assert_eq!(publisher.channel().len(), 0);
1169
1170        // 7. Verify that reading again results in an error because the channel is empty and we are not closing it
1171        // In a real scenario with a closed channel, this would error out. Here we can just check it's empty.
1172        // A `receive` call would just hang, waiting for a message.
1173    }
1174
1175    #[tokio::test]
1176    async fn test_memory_subscriber_structure() {
1177        let cfg = MemoryConfig {
1178            topic: "base_topic".to_string(),
1179            capacity: Some(10),
1180            ..Default::default()
1181        };
1182        let subscriber_id = "sub1";
1183        let mut subscriber = MemorySubscriber::new(&cfg, subscriber_id).unwrap();
1184
1185        // The subscriber should be listening on "base_topic-sub1"
1186        // We can verify this by creating a publisher for that specific topic.
1187        let pub_cfg = MemoryConfig {
1188            topic: format!("base_topic-{}", subscriber_id),
1189            capacity: Some(10),
1190            ..Default::default()
1191        };
1192        let publisher = MemoryPublisher::new(&pub_cfg).unwrap();
1193
1194        publisher.send("hello subscriber".into()).await.unwrap();
1195
1196        let received = subscriber.receive().await.unwrap();
1197        assert_eq!(received.message.get_payload_str(), "hello subscriber");
1198    }
1199
1200    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1201    async fn test_memory_request_reply_mode() {
1202        let topic = format!("mem_rr_topic_{}", fast_uuid_v7::gen_id_str());
1203        let input_endpoint = Endpoint::new_memory(&topic, 10);
1204        let output_endpoint = Endpoint::new_response();
1205        let handler = |mut msg: CanonicalMessage| async move {
1206            let request_payload = msg.get_payload_str();
1207            let response_payload = format!("reply to {}", request_payload);
1208            msg.set_payload_str(response_payload);
1209            Ok(Handled::Publish(msg))
1210        };
1211
1212        let route = Route::new(input_endpoint, output_endpoint).with_handler(handler);
1213        route.deploy("mem_rr_test").await.unwrap();
1214
1215        // Create a publisher with request_reply = true
1216        let publisher = MemoryPublisher::new(&MemoryConfig {
1217            topic: topic.clone(),
1218            capacity: Some(10),
1219            request_reply: true,
1220            request_timeout_ms: Some(2000),
1221            ..Default::default()
1222        })
1223        .unwrap();
1224
1225        let result = publisher.send("direct request".into()).await.unwrap();
1226
1227        if let Sent::Response(response_msg) = result {
1228            assert_eq!(response_msg.get_payload_str(), "reply to direct request");
1229        } else {
1230            panic!("Expected Sent::Response, got {:?}", result);
1231        }
1232
1233        // Clean up
1234        Route::stop("mem_rr_test").await;
1235    }
1236
1237    #[tokio::test]
1238    async fn test_memory_request_reply_timeout_cleans_waiter() {
1239        let topic = format!("mem_rr_timeout_{}", fast_uuid_v7::gen_id_str());
1240        let correlation_id = fast_uuid_v7::gen_id_string();
1241        let publisher = MemoryPublisher::new(&MemoryConfig {
1242            topic: topic.clone(),
1243            capacity: Some(10),
1244            request_reply: true,
1245            request_timeout_ms: Some(25),
1246            ..Default::default()
1247        })
1248        .unwrap();
1249
1250        let mut message = CanonicalMessage::from("request with no responder");
1251        message
1252            .metadata
1253            .insert("correlation_id".to_string(), correlation_id.clone());
1254
1255        let err = publisher.send(message).await.unwrap_err();
1256        assert!(err
1257            .to_string()
1258            .contains("Request timed out waiting for response"));
1259
1260        let response_channel = get_or_create_response_channel(&topic);
1261        assert!(
1262            response_channel
1263                .remove_waiter(&correlation_id)
1264                .await
1265                .is_none(),
1266            "timed out request should clean up the registered waiter"
1267        );
1268    }
1269
1270    #[tokio::test]
1271    async fn test_memory_nack_requeue() {
1272        let topic = format!("test_nack_requeue_{}", fast_uuid_v7::gen_id_str());
1273        let config = MemoryConfig {
1274            topic: topic.clone(),
1275            capacity: Some(10),
1276            enable_nack: true,
1277            ..Default::default()
1278        };
1279        let mut consumer = MemoryConsumer::new(&config).unwrap();
1280        let publisher = MemoryPublisher::new_local(&topic, 10);
1281
1282        publisher.send("to_be_nacked".into()).await.unwrap();
1283
1284        // 1. Receive and Nack
1285        let received1 = consumer.receive().await.unwrap();
1286        assert_eq!(received1.message.get_payload_str(), "to_be_nacked");
1287        (received1.commit)(crate::traits::MessageDisposition::Nack)
1288            .await
1289            .unwrap();
1290
1291        // 2. Receive again (should be re-queued)
1292        let received2 = tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive())
1293            .await
1294            .expect("Timed out waiting for re-queued message")
1295            .unwrap();
1296        assert_eq!(received2.message.get_payload_str(), "to_be_nacked");
1297
1298        // 3. Ack
1299        (received2.commit)(crate::traits::MessageDisposition::Ack)
1300            .await
1301            .unwrap();
1302
1303        // 4. Verify empty
1304        let result =
1305            tokio::time::timeout(std::time::Duration::from_millis(100), consumer.receive()).await;
1306        assert!(result.is_err(), "Channel should be empty");
1307    }
1308
1309    #[tokio::test]
1310    async fn test_memory_dropped_batch_requeues_messages() {
1311        let topic = format!("drop_requeue_{}", fast_uuid_v7::gen_id_str());
1312        let config = MemoryConfig {
1313            topic: topic.clone(),
1314            capacity: Some(10),
1315            enable_nack: true,
1316            ..Default::default()
1317        };
1318        let mut consumer = MemoryConsumer::new(&config).unwrap();
1319        let publisher = MemoryPublisher::new_local(&topic, 10);
1320
1321        publisher
1322            .send_batch(vec!["first".into(), "second".into()])
1323            .await
1324            .unwrap();
1325
1326        let batch = consumer.receive_batch(2).await.unwrap();
1327        assert_eq!(batch.messages.len(), 2);
1328        drop(batch);
1329
1330        let requeued =
1331            tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive_batch(2))
1332                .await
1333                .expect("Timed out waiting for dropped batch to be re-queued")
1334                .unwrap();
1335
1336        assert_eq!(
1337            requeued
1338                .messages
1339                .iter()
1340                .map(CanonicalMessage::get_payload_str)
1341                .collect::<Vec<_>>(),
1342            vec!["first".to_string(), "second".to_string()]
1343        );
1344
1345        (requeued.commit)(vec![MessageDisposition::Ack, MessageDisposition::Ack])
1346            .await
1347            .unwrap();
1348    }
1349
1350    #[tokio::test]
1351    async fn test_memory_batch_commit_rejects_mismatched_dispositions() {
1352        let topic = format!("commit_mismatch_{}", fast_uuid_v7::gen_id_str());
1353        let config = MemoryConfig {
1354            topic: topic.clone(),
1355            capacity: Some(10),
1356            enable_nack: true,
1357            ..Default::default()
1358        };
1359        let mut consumer = MemoryConsumer::new(&config).unwrap();
1360        let publisher = MemoryPublisher::new_local(&topic, 10);
1361
1362        publisher
1363            .send_batch(vec!["one".into(), "two".into()])
1364            .await
1365            .unwrap();
1366
1367        let batch = consumer.receive_batch(2).await.unwrap();
1368        let err = (batch.commit)(vec![MessageDisposition::Ack])
1369            .await
1370            .unwrap_err();
1371        assert!(err
1372            .to_string()
1373            .contains("Memory batch commit received mismatched disposition count"));
1374
1375        let retried =
1376            tokio::time::timeout(std::time::Duration::from_secs(1), consumer.receive_batch(2))
1377                .await
1378                .expect("Timed out waiting for mismatched commit batch to be re-queued")
1379                .unwrap();
1380        assert_eq!(retried.messages.len(), 2);
1381        (retried.commit)(vec![MessageDisposition::Ack, MessageDisposition::Ack])
1382            .await
1383            .unwrap();
1384    }
1385
1386    #[tokio::test]
1387    async fn test_memory_event_store_integration() {
1388        let topic = "event_store_test";
1389        // Publisher with subscribe_mode=true enables EventStore writing
1390        let pub_config = MemoryConfig {
1391            topic: topic.to_string(),
1392            subscribe_mode: true,
1393            ..Default::default()
1394        };
1395        let publisher = MemoryPublisher::new(&pub_config).unwrap();
1396
1397        // Subscriber 1
1398        let mut sub1 = MemorySubscriber::new(&pub_config, "sub1").unwrap();
1399        // Subscriber 2
1400        let mut sub2 = MemorySubscriber::new(&pub_config, "sub2").unwrap();
1401
1402        publisher.send("event1".into()).await.unwrap();
1403
1404        let msg1 = sub1.receive().await.unwrap();
1405        assert_eq!(msg1.message.get_payload_str(), "event1");
1406        (msg1.commit)(MessageDisposition::Ack).await.unwrap();
1407
1408        let msg2 = sub2.receive().await.unwrap();
1409        assert_eq!(msg2.message.get_payload_str(), "event1");
1410    }
1411
1412    #[tokio::test]
1413    async fn test_memory_no_subscribers_persistence() {
1414        let topic = format!("no_subs_{}", fast_uuid_v7::gen_id_str());
1415        let pub_config = MemoryConfig {
1416            topic: topic.clone(),
1417            subscribe_mode: true,
1418            ..Default::default()
1419        };
1420
1421        // 1. Create Publisher (Log mode)
1422        let publisher = MemoryPublisher::new(&pub_config).unwrap();
1423
1424        // 2. Publish messages with no subscribers
1425        publisher.send("msg1".into()).await.unwrap();
1426        publisher.send("msg2".into()).await.unwrap();
1427
1428        // 3. Create Subscriber (Late joiner)
1429        let sub_config = MemoryConfig {
1430            topic: topic.clone(),
1431            subscribe_mode: true,
1432            ..Default::default()
1433        };
1434        let mut subscriber = MemorySubscriber::new(&sub_config, "late_sub").unwrap();
1435
1436        // 4. Verify messages are received
1437        let received1 = subscriber.receive().await.unwrap();
1438        assert_eq!(received1.message.get_payload_str(), "msg1");
1439        (received1.commit)(MessageDisposition::Ack).await.unwrap();
1440
1441        let received2 = subscriber.receive().await.unwrap();
1442        assert_eq!(received2.message.get_payload_str(), "msg2");
1443        (received2.commit)(MessageDisposition::Ack).await.unwrap();
1444    }
1445
1446    #[tokio::test]
1447    async fn test_memory_mixed_mode_error() {
1448        let topic_q = format!("mixed_q_{}", fast_uuid_v7::gen_id_str());
1449        let topic_l = format!("mixed_l_{}", fast_uuid_v7::gen_id_str());
1450
1451        // Case 1: Active Queue, try to create Log Consumer
1452        let _pub_q = MemoryPublisher::new_local(&topic_q, 10); // Creates Queue backend
1453
1454        let log_conf = MemoryConfig {
1455            topic: topic_q.clone(),
1456            subscribe_mode: true,
1457            ..Default::default()
1458        };
1459        let err = MemoryConsumer::new(&log_conf);
1460        assert!(err.is_err());
1461        assert!(err
1462            .unwrap_err()
1463            .to_string()
1464            .contains("already active as a Queue"));
1465
1466        // Case 2: Active Log, try to create Queue Consumer
1467        let log_pub_conf = MemoryConfig {
1468            topic: topic_l.clone(),
1469            subscribe_mode: true,
1470            ..Default::default()
1471        };
1472        let _pub_l = MemoryPublisher::new(&log_pub_conf).unwrap(); // Creates Log backend
1473
1474        let queue_conf = MemoryConfig {
1475            topic: topic_l.clone(),
1476            subscribe_mode: false,
1477            ..Default::default()
1478        };
1479        let err = MemoryConsumer::new(&queue_conf);
1480        assert!(err.is_err());
1481        assert!(err
1482            .unwrap_err()
1483            .to_string()
1484            .contains("already active as a Subscriber Log"));
1485    }
1486
1487    #[tokio::test]
1488    async fn test_memory_publisher_mixed_mode_error() {
1489        let topic_q = format!("pub_mixed_q_{}", fast_uuid_v7::gen_id_str());
1490
1491        // 1. Create a Queue Consumer to establish the channel
1492        let _cons_q = MemoryConsumer::new_local(&topic_q, 10);
1493
1494        // 2. Try to create a Log Publisher on the same topic
1495        let log_conf = MemoryConfig {
1496            topic: topic_q.clone(),
1497            subscribe_mode: true,
1498            ..Default::default()
1499        };
1500        let err = MemoryPublisher::new(&log_conf);
1501        assert!(err.is_err());
1502        assert!(err
1503            .unwrap_err()
1504            .to_string()
1505            .contains("already active as a Queue"));
1506    }
1507
1508    #[tokio::test]
1509    async fn test_memory_publisher_adaptive_behavior() {
1510        let topic = format!("adaptive_{}", fast_uuid_v7::gen_id_str());
1511
1512        // 1. Create a Log Consumer (Subscriber) to establish the EventStore
1513        let sub_config = MemoryConfig {
1514            topic: topic.clone(),
1515            subscribe_mode: true,
1516            ..Default::default()
1517        };
1518        let mut subscriber = MemorySubscriber::new(&sub_config, "sub1").unwrap();
1519
1520        // 2. Create a Publisher WITHOUT subscribe_mode explicitly set
1521        let pub_config = MemoryConfig {
1522            topic: topic.clone(),
1523            subscribe_mode: false, // Default is false
1524            ..Default::default()
1525        };
1526        // This should succeed and adapt to Log mode because the store exists
1527        let publisher = MemoryPublisher::new(&pub_config).unwrap();
1528
1529        // 3. Verify it publishes to the store (subscriber receives it)
1530        publisher.send("adaptive_msg".into()).await.unwrap();
1531
1532        let received = subscriber.receive().await.unwrap();
1533        assert_eq!(received.message.get_payload_str(), "adaptive_msg");
1534    }
1535}