p2p_chat/logging/
buffer.rs

1//! This module provides a buffer for storing and managing log entries.
2//!
3//! The `LogBuffer` is used to collect log entries and send them to the UI in
4//! batches to avoid overwhelming the UI with too many updates.
5use crate::ui::{LogEntry, UIMode};
6use std::collections::VecDeque;
7use std::sync::{Arc, Mutex};
8use std::time::Duration;
9use tokio::sync::mpsc;
10use tokio::time::{interval, Interval};
11use tracing::Level;
12
13/// A buffer for storing and managing log entries.
14pub struct LogBuffer {
15    /// The circular buffer of log entries.
16    entries: Arc<Mutex<VecDeque<LogEntry>>>,
17    /// The maximum number of entries to store in the buffer.
18    max_size: usize,
19    /// The sender for sending UI events.
20    ui_sender: Arc<Mutex<Option<mpsc::UnboundedSender<crate::ui::UIEvent>>>>,
21    /// The current log level to display in the UI.
22    current_display_level: Arc<Mutex<Level>>,
23    /// The current UI mode.
24    current_ui_mode: Arc<Mutex<UIMode>>,
25    /// A batch of pending log entries to be sent to the UI.
26    pending_batch: Arc<Mutex<Vec<LogEntry>>>,
27    /// The timer for sending log batches.
28    batch_timer: Arc<Mutex<Option<Interval>>>,
29}
30
31impl LogBuffer {
32    /// Creates a new `LogBuffer`.
33    ///
34    /// # Arguments
35    ///
36    /// * `max_size` - The maximum number of entries to store in the buffer.
37    pub fn new(max_size: usize) -> Self {
38        Self {
39            entries: Arc::new(Mutex::new(VecDeque::with_capacity(max_size))),
40            max_size,
41            ui_sender: Arc::new(Mutex::new(None)),
42            current_display_level: Arc::new(Mutex::new(Level::DEBUG)),
43            current_ui_mode: Arc::new(Mutex::new(UIMode::Chat)),
44            pending_batch: Arc::new(Mutex::new(Vec::new())),
45            batch_timer: Arc::new(Mutex::new(None)),
46        }
47    }
48
49    /// Sets the UI sender.
50    ///
51    /// This is used to send events to the UI thread.
52    pub fn set_ui_sender(&self, sender: mpsc::UnboundedSender<crate::ui::UIEvent>) {
53        *self.ui_sender.lock().unwrap() = Some(sender);
54    }
55
56    /// Adds a new log entry to the buffer.
57    ///
58    /// The entry is always stored in the buffer, but it is only sent to the UI
59    /// if its level is at or above the current display level.
60    pub fn add_entry(&self, entry: LogEntry) {
61        // Always store the log entry regardless of display level.
62        {
63            let mut entries = self.entries.lock().unwrap();
64            if entries.len() >= self.max_size {
65                entries.pop_front();
66            }
67            entries.push_back(entry.clone());
68        }
69
70        // Notify the UI for logs that meet the display level.
71        let should_notify = {
72            let current_level = *self.current_display_level.lock().unwrap();
73            entry.level <= current_level
74        };
75
76        if should_notify {
77            // Add to pending batch for async processing.
78            self.pending_batch.lock().unwrap().push(entry);
79
80            // Start batch timer if not already running.
81            self.start_batch_timer_if_needed();
82        }
83    }
84
85    /// Sets the current display level for filtering UI notifications.
86    pub fn set_display_level(&self, level: Level) {
87        *self.current_display_level.lock().unwrap() = level;
88
89        // Trigger a refresh of logs with the new level.
90        if let Some(ref sender) = *self.ui_sender.lock().unwrap() {
91            let _ = sender.send(crate::ui::UIEvent::RefreshLogs);
92        }
93    }
94
95    /// Sets the current UI mode to optimize notifications.
96    pub fn set_ui_mode(&self, mode: UIMode) {
97        *self.current_ui_mode.lock().unwrap() = mode.clone();
98
99        // If switching to log mode, trigger a refresh.
100        if matches!(mode, UIMode::Logs { .. }) {
101            if let Some(ref sender) = *self.ui_sender.lock().unwrap() {
102                let _ = sender.send(crate::ui::UIEvent::RefreshLogs);
103            }
104        }
105    }
106
107    /// Starts the batch timer if it's not already running.
108    fn start_batch_timer_if_needed(&self) {
109        let mut timer_guard = self.batch_timer.lock().unwrap();
110        if timer_guard.is_none() {
111            *timer_guard = Some(interval(Duration::from_millis(100))); // Batch every 100ms
112
113            // Clone necessary data for the async task.
114            let ui_sender = self.ui_sender.clone();
115            let pending_batch = self.pending_batch.clone();
116            let batch_timer = self.batch_timer.clone();
117
118            drop(timer_guard); // Release the lock before spawning.
119
120            // Spawn an async task to flush batches.
121            tokio::spawn(async move {
122                let mut timer = {
123                    let mut timer_guard = batch_timer.lock().unwrap();
124                    timer_guard.take().unwrap()
125                };
126
127                timer.tick().await; // Skip the first immediate tick.
128
129                loop {
130                    timer.tick().await;
131
132                    // Check if there are pending entries to flush.
133                    let batch = {
134                        let mut pending = pending_batch.lock().unwrap();
135                        if pending.is_empty() {
136                            continue;
137                        }
138                        let batch = pending.drain(..).collect::<Vec<_>>();
139                        batch
140                    };
141
142                    // Send batch to UI.
143                    if let Some(ref sender) = *ui_sender.lock().unwrap() {
144                        if sender.send(crate::ui::UIEvent::NewLogBatch(batch)).is_err() {
145                            // Channel closed, stop the timer.
146                            break;
147                        }
148                    } else {
149                        // No sender available, stop the timer.
150                        break;
151                    }
152                }
153
154                // Clean up timer when done.
155                *batch_timer.lock().unwrap() = None;
156            });
157        }
158    }
159}