pub mod snapshot;
use std::sync::Arc;
use tokio::sync::mpsc;
pub use snapshot::TuiSnapshot;
const FLUSH_QUEUE_CAPACITY: usize = 100;
pub struct SaveCoordinator {
save_tx: Option<mpsc::Sender<()>>,
save_completion_tx: Option<mpsc::UnboundedSender<()>>,
pending_saves: usize,
file_watcher: Option<Arc<kanban_persistence::FileWatcher>>,
}
impl SaveCoordinator {
#[allow(clippy::type_complexity)]
pub fn new(
has_persistence: bool,
) -> (
Self,
Option<mpsc::Receiver<()>>,
Option<mpsc::UnboundedReceiver<()>>,
) {
let (save_tx, save_rx) = if has_persistence {
let (tx, rx) = mpsc::channel(FLUSH_QUEUE_CAPACITY);
(Some(tx), Some(rx))
} else {
(None, None)
};
let (save_completion_tx, save_completion_rx) = mpsc::unbounded_channel();
let coordinator = Self {
save_tx,
save_completion_tx: Some(save_completion_tx),
pending_saves: 0,
file_watcher: None,
};
(coordinator, save_rx, Some(save_completion_rx))
}
pub fn close_save_channel(&mut self) {
self.save_tx = None;
}
pub fn has_save_channel(&self) -> bool {
self.save_tx.is_some()
}
pub fn queue_flush(&mut self) {
if let Some(ref tx) = self.save_tx {
tracing::debug!(
"Queueing flush signal (pending: {} -> {})",
self.pending_saves,
self.pending_saves + 1
);
match tx.try_send(()) {
Ok(_) => {
self.pending_saves += 1;
tracing::debug!("Flush signal queued successfully");
}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
tracing::warn!(
"Save queue is full ({} pending), skipping this flush signal. \
This may indicate the disk is slow or the save worker is overloaded.",
self.pending_saves
);
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
tracing::error!("Failed to queue flush signal: channel closed");
}
}
} else {
tracing::debug!("No save channel available - skipping save");
}
}
pub fn has_pending_saves(&self) -> bool {
self.pending_saves > 0
}
pub fn save_completed(&mut self) {
if self.pending_saves > 0 {
self.pending_saves -= 1;
tracing::debug!(
"Save completed (pending: {} -> {})",
self.pending_saves + 1,
self.pending_saves
);
}
}
pub fn save_completion_tx(&self) -> Option<&mpsc::UnboundedSender<()>> {
self.save_completion_tx.as_ref()
}
#[doc(hidden)]
pub fn set_pending_for_test(&mut self, n: usize) {
self.pending_saves = n;
}
pub fn set_file_watcher(&mut self, watcher: Arc<kanban_persistence::FileWatcher>) {
self.file_watcher = Some(watcher);
tracing::debug!("File watcher set on SaveCoordinator");
}
#[allow(clippy::type_complexity)]
pub fn reset_save_channels(&mut self) -> (mpsc::Receiver<()>, mpsc::UnboundedReceiver<()>) {
self.file_watcher = None;
self.pending_saves = 0;
let (tx, rx) = mpsc::channel(FLUSH_QUEUE_CAPACITY);
self.save_tx = Some(tx);
let (completion_tx, completion_rx) = mpsc::unbounded_channel();
self.save_completion_tx = Some(completion_tx);
(rx, completion_rx)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_queue_flush_does_not_open_suppress_window() {
let (mut coordinator, _rx, _crx) = SaveCoordinator::new(true);
let watcher = Arc::new(kanban_persistence::FileWatcher::new());
coordinator.set_file_watcher(Arc::clone(&watcher));
coordinator.queue_flush();
assert!(
!watcher.is_suppressing(),
"queue_flush must not open the suppress window"
);
}
#[test]
fn test_save_coordinator_creation_no_persistence() {
let (coordinator, save_rx, _completion_rx) = SaveCoordinator::new(false);
assert!(!coordinator.has_pending_saves());
assert!(save_rx.is_none());
}
#[test]
fn test_save_coordinator_creation_with_persistence() {
let (coordinator, save_rx, _completion_rx) = SaveCoordinator::new(true);
assert!(!coordinator.has_pending_saves());
assert!(save_rx.is_some());
assert!(coordinator.has_save_channel());
}
#[test]
fn test_reset_save_channels() {
let (mut coordinator, _rx, _crx) = SaveCoordinator::new(true);
let (_rx, _crx) = coordinator.reset_save_channels();
assert!(!coordinator.has_pending_saves());
assert!(coordinator.has_save_channel());
}
}