use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Clone, Default)]
pub struct WorkspaceLockManager {
locks: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
}
impl WorkspaceLockManager {
pub fn new() -> Self {
Self::default()
}
pub async fn get_lock(&self, project: &str) -> Arc<Mutex<()>> {
let mut map = self.locks.lock().await;
map.entry(project.to_string())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone()
}
pub async fn is_locked(&self, project: &str) -> bool {
let map = self.locks.lock().await;
map.get(project)
.map(|m| m.try_lock().is_err())
.unwrap_or(false)
}
pub async fn locked_projects(&self) -> Vec<String> {
let map = self.locks.lock().await;
map.iter()
.filter(|(_, mutex)| mutex.try_lock().is_err())
.map(|(name, _)| name.clone())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_workspace_lock_manager_same_project_shares_mutex() {
let manager = WorkspaceLockManager::new();
let lock1 = manager.get_lock("my-project").await;
let lock2 = manager.get_lock("my-project").await;
let guard1 = lock1.try_lock();
assert!(guard1.is_ok());
let guard2 = lock2.try_lock();
assert!(guard2.is_err());
drop(guard1);
let guard2_retry = lock2.try_lock();
assert!(guard2_retry.is_ok());
}
#[tokio::test]
async fn test_workspace_lock_manager_different_projects_independent() {
let manager = WorkspaceLockManager::new();
let lock1 = manager.get_lock("project-a").await;
let lock2 = manager.get_lock("project-b").await;
let guard1 = lock1.try_lock();
let guard2 = lock2.try_lock();
assert!(guard1.is_ok());
assert!(guard2.is_ok());
}
#[tokio::test]
async fn test_is_locked_reflects_held_locks_without_inserting() {
let manager = WorkspaceLockManager::new();
assert!(!manager.is_locked("ghost-project").await);
let lock = manager.get_lock("busy-project").await;
let guard = lock.try_lock().unwrap();
assert!(manager.is_locked("busy-project").await);
let locked = manager.locked_projects().await;
assert_eq!(locked, vec!["busy-project".to_string()]);
drop(guard);
assert!(!manager.is_locked("busy-project").await);
assert!(manager.locked_projects().await.is_empty());
}
}