chat_platform_wasm/
tab_lock.rs

1//! Browser tab lock using BroadcastChannel API.
2//!
3//! Only one browser tab can hold the lock at a time. When a second tab tries
4//! to acquire the lock, it will fail immediately.
5
6#[cfg(target_arch = "wasm32")]
7use wasm_bindgen::prelude::*;
8#[cfg(target_arch = "wasm32")]
9use wasm_bindgen::closure::Closure;
10#[cfg(target_arch = "wasm32")]
11use web_sys::BroadcastChannel;
12
13/// A handle representing the acquired tab lock. Dropping it releases the lock.
14pub struct TabLock {
15    #[cfg(target_arch = "wasm32")]
16    _channel: BroadcastChannel,
17    #[cfg(not(target_arch = "wasm32"))]
18    _nop: (),
19}
20
21impl TabLock {
22    /// Try to acquire the tab lock. Returns `Some(TabLock)` on success,
23    /// `None` if another tab already holds the lock.
24    #[cfg(target_arch = "wasm32")]
25    pub fn try_acquire(lock_name: &str) -> Option<Self> {
26        let channel = match BroadcastChannel::new(lock_name) {
27            Ok(c) => c,
28            Err(_) => return None,
29        };
30
31        // Send a ping to check if another tab is alive.
32        let has_other = std::cell::Cell::new(false);
33        let has_other_ref = &has_other;
34
35        let on_message = Closure::wrap(Box::new(move |event: web_sys::MessageEvent| {
36            if let Some(data) = event.data().as_string()
37                && data == "pong"
38            {
39                has_other_ref.set(true);
40            }
41        }) as Box<dyn FnMut(_)>);
42
43        channel.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
44
45        // Send ping
46        let _ = channel.post_message(&JsValue::from_str("ping"));
47
48        // Give other tabs a brief moment to respond.
49        let start = js_sys::Date::now();
50        while js_sys::Date::now() - start < 100.0 && !has_other.get() {
51            // Spin briefly
52        }
53
54        channel.set_onmessage(None);
55        on_message.forget();
56
57        if has_other.get() {
58            return None;
59        }
60
61        // We hold the lock. Set up a responder so other tabs know we're here.
62        let channel_for_pong = channel.clone();
63        let on_pong = Closure::wrap(Box::new(move |event: web_sys::MessageEvent| {
64            if let Some(data) = event.data().as_string()
65                && data == "ping"
66            {
67                let _ = channel_for_pong.post_message(&JsValue::from_str("pong"));
68            }
69        }) as Box<dyn FnMut(_)>);
70
71        channel.set_onmessage(Some(on_pong.as_ref().unchecked_ref()));
72        on_pong.forget();
73
74        Some(TabLock { _channel: channel })
75    }
76
77    /// Non-WASM stub: always succeeds so the crate can be type-checked on native.
78    #[cfg(not(target_arch = "wasm32"))]
79    pub fn try_acquire(_lock_name: &str) -> Option<Self> {
80        Some(TabLock { _nop: () })
81    }
82}
83
84#[cfg(target_arch = "wasm32")]
85impl Drop for TabLock {
86    fn drop(&mut self) {
87        self._channel.close();
88    }
89}