chat_platform_native/
tab_lock.rs

1use std::path::Path;
2
3/// A cross-platform tab-lock (single-instance lock).
4///
5/// On native targets this uses an exclusive file lock via `fs2`.
6/// On `wasm32` this is a no-op stub because only one WASM instance runs
7/// per browser tab anyway; Phase 5 may add `web-sys` BroadcastChannel locking.
8#[cfg(not(target_arch = "wasm32"))]
9pub struct TabLock {
10    _file: std::fs::File,
11    path: std::path::PathBuf,
12}
13
14#[cfg(target_arch = "wasm32")]
15pub struct TabLock;
16
17impl TabLock {
18    /// Attempt to acquire the lock. Returns `Some(TabLock)` on success,
19    /// or `None` if another instance already holds the lock.
20    pub fn try_acquire(path: &Path) -> Option<TabLock> {
21        #[cfg(not(target_arch = "wasm32"))]
22        {
23            use fs2::FileExt;
24            let file = std::fs::OpenOptions::new()
25                .write(true)
26                .create(true)
27                .truncate(false)
28                .open(path)
29                .ok()?;
30            file.try_lock_exclusive().ok()?;
31            Some(TabLock {
32                _file: file,
33                path: path.to_path_buf(),
34            })
35        }
36        #[cfg(target_arch = "wasm32")]
37        {
38            let _ = path;
39            Some(TabLock)
40        }
41    }
42}
43
44#[cfg(not(target_arch = "wasm32"))]
45impl Drop for TabLock {
46    fn drop(&mut self) {
47        #[allow(unused_imports)]
48        use fs2::FileExt;
49        // Best-effort unlock + cleanup
50        let _ = self._file.unlock();
51        let _ = std::fs::remove_file(&self.path);
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn try_acquire_returns_lock_when_file_is_free() {
61        let dir = std::env::temp_dir();
62        let path = dir.join("p2pandemonium-tab-lock-test-1.lock");
63        let _ = std::fs::remove_file(&path);
64        let lock = TabLock::try_acquire(&path);
65        assert!(lock.is_some());
66        drop(lock);
67        let _ = std::fs::remove_file(&path);
68    }
69
70    #[test]
71    fn second_acquire_fails_when_lock_is_held() {
72        let dir = std::env::temp_dir();
73        let path = dir.join("p2pandemonium-tab-lock-test-2.lock");
74        let _ = std::fs::remove_file(&path);
75
76        let lock1 = TabLock::try_acquire(&path);
77        assert!(lock1.is_some());
78
79        let lock2 = TabLock::try_acquire(&path);
80        assert!(lock2.is_none());
81
82        drop(lock1);
83        let _ = std::fs::remove_file(&path);
84    }
85
86    #[test]
87    fn lock_can_be_reacquired_after_release() {
88        let dir = std::env::temp_dir();
89        let path = dir.join("p2pandemonium-tab-lock-test-3.lock");
90        let _ = std::fs::remove_file(&path);
91
92        {
93            let lock = TabLock::try_acquire(&path);
94            assert!(lock.is_some());
95        }
96
97        let lock = TabLock::try_acquire(&path);
98        assert!(lock.is_some());
99        drop(lock);
100        let _ = std::fs::remove_file(&path);
101    }
102}