ic_auth_client/
idle_manager.rs

1use std::sync::mpsc;
2use std::{
3    mem,
4    sync::{Arc, Mutex},
5};
6use wasm_bindgen::{closure::Closure, prelude::*};
7use wasm_bindgen_futures::spawn_local;
8use web_sys::{Event, Window};
9
10type Callback = Box<dyn FnMut() + Send>;
11type JsCallback = Closure<dyn FnMut(Event)>;
12
13const EVENTS: [&str; 6] = [
14    "load",
15    "mousedown",
16    "mousemove",
17    "keydown",
18    "touchstart",
19    "wheel",
20];
21
22/// The relevant state of JavaScript
23struct JsContext {
24    event_handlers: Vec<(String, JsCallback)>,
25    window: Window,
26    is_initialized: bool,
27}
28
29impl JsContext {
30    fn new() -> Self {
31        Self {
32            event_handlers: Vec::new(),
33            window: web_sys::window().expect("should have a Window"),
34            is_initialized: false,
35        }
36    }
37
38    fn add_event_listener(&mut self, event_type: &str, callback: JsCallback) {
39        self.window
40            .add_event_listener_with_callback(event_type, callback.as_ref().unchecked_ref())
41            .expect("should add event listener");
42        self.event_handlers.push((event_type.to_string(), callback));
43    }
44
45    fn remove_all_listeners(&mut self) {
46        for (event_type, handler) in self.event_handlers.drain(..) {
47            self.window
48                .remove_event_listener_with_callback(&event_type, handler.as_ref().unchecked_ref())
49                .expect("should remove event listener");
50        }
51    }
52
53    fn clear_timeout(&self, timeout_id: i32) {
54        self.window.clear_timeout_with_handle(timeout_id);
55    }
56
57    fn set_timeout(&self, closure: &Closure<dyn FnMut()>, timeout: i32) -> Result<i32, JsValue> {
58        self.window
59            .set_timeout_with_callback_and_timeout_and_arguments_0(
60                closure.as_ref().unchecked_ref(),
61                timeout,
62            )
63    }
64}
65
66impl Drop for JsContext {
67    fn drop(&mut self) {
68        self.remove_all_listeners();
69    }
70}
71
72// Context contains only states that can be shared among threads
73#[derive(Default)]
74struct Context {
75    callbacks: Arc<Mutex<Vec<Callback>>>,
76}
77
78enum JsMessage {
79    ResetTimer(u32),
80    Cleanup,
81    ScrollDebounce(u32),
82}
83
84struct JsHandler {
85    context: JsContext,
86    receiver: mpsc::Receiver<JsMessage>,
87    sender: mpsc::Sender<JsMessage>,
88    current_timer: Option<i32>,
89    current_scroll_debounce_timer: Option<i32>,
90    exit_closure: Option<Closure<dyn FnMut()>>,
91    reset_closure: Option<Closure<dyn FnMut()>>,
92}
93
94impl JsHandler {
95    fn new(receiver: mpsc::Receiver<JsMessage>, sender: mpsc::Sender<JsMessage>) -> Self {
96        Self {
97            context: JsContext::new(),
98            receiver,
99            sender,
100            current_timer: None,
101            current_scroll_debounce_timer: None,
102            exit_closure: None,
103            reset_closure: None,
104        }
105    }
106
107    fn get_handler_sender(&self) -> mpsc::Sender<JsMessage> {
108        self.sender.clone()
109    }
110
111    async fn run(&mut self) {
112        while let Ok(msg) = self.receiver.recv() {
113            match msg {
114                JsMessage::ResetTimer(timeout) => self.handle_reset_timer(timeout),
115                JsMessage::Cleanup => self.handle_cleanup(),
116                JsMessage::ScrollDebounce(delay) => self.handle_scroll_debounce(delay),
117            }
118        }
119    }
120
121    fn handle_reset_timer(&mut self, timeout: u32) {
122        // Clear existing timeout if it exists
123        if let Some(timer_id) = self.current_timer.take() {
124            self.context.clear_timeout(timer_id);
125        }
126
127        // If timeout is 0, just reset the timer without setting a new one
128        if timeout == 0 {
129            return;
130        }
131
132        // Create a sender for the IdleManager to send the Cleanup message
133        let (sender, oneshot_receiver) = mpsc::channel();
134
135        // Use a closure to handle the timeout
136        let exit_closure = Closure::once(move || {
137            // Send a message that will be received by the oneshot_receiver
138            let _ = sender.send(());
139        });
140
141        // Set the timeout with the closure
142        match self.context.set_timeout(&exit_closure, timeout as i32) {
143            Ok(timer_id) => {
144                self.current_timer = Some(timer_id);
145                self.exit_closure = Some(exit_closure);
146
147                // Spawn a task to handle the oneshot receiver
148                let sender = self.get_handler_sender();
149                spawn_local(async move {
150                    // Wait for the oneshot message
151                    let _ = oneshot_receiver.recv();
152                    // Then send the cleanup message to the handler
153                    let _ = sender.send(JsMessage::Cleanup);
154                });
155            }
156            Err(_) => {
157                // If setting timeout fails, drop the closure
158                drop(exit_closure);
159            }
160        }
161    }
162
163    fn handle_cleanup(&mut self) {
164        // Clear existing timeouts
165        if let Some(timer_id) = self.current_timer.take() {
166            self.context.clear_timeout(timer_id);
167        }
168
169        if let Some(timer_id) = self.current_scroll_debounce_timer.take() {
170            self.context.clear_timeout(timer_id);
171        }
172
173        // Remove all event listeners
174        self.context.remove_all_listeners();
175
176        // Drop closures
177        self.exit_closure = None;
178        self.reset_closure = None;
179    }
180
181    fn handle_scroll_debounce(&mut self, delay: u32) {
182        // Clear existing scroll debounce timeout if it exists
183        if let Some(timer_id) = self.current_scroll_debounce_timer.take() {
184            self.context.clear_timeout(timer_id);
185        }
186
187        // Create a sender for the debounce timer
188        let (sender, oneshot_receiver) = mpsc::channel();
189
190        // Use a closure to handle the timeout
191        let reset_closure = Closure::once(move || {
192            // Send a message that will be received by the oneshot_receiver
193            let _ = sender.send(());
194        });
195
196        // Set the timeout with the closure
197        match self.context.set_timeout(&reset_closure, delay as i32) {
198            Ok(timer_id) => {
199                self.current_scroll_debounce_timer = Some(timer_id);
200                self.reset_closure = Some(reset_closure);
201
202                // Spawn a task to handle the oneshot receiver
203                let sender = self.get_handler_sender();
204                spawn_local(async move {
205                    // Wait for the oneshot message
206                    let _ = oneshot_receiver.recv();
207                    // Then send the reset timer message to the handler
208                    let _ = sender.send(JsMessage::ResetTimer(0));
209                });
210            }
211            Err(_) => {
212                // If setting timeout fails, drop the closure
213                drop(reset_closure);
214            }
215        }
216    }
217}
218
219/// IdleManager is a struct that manages idle state and events.
220/// It provides functionality to register callbacks that are triggered when the system becomes idle,
221/// and to reset the idle timer when certain events occur.
222#[derive(Clone)]
223pub struct IdleManager {
224    context: Arc<Mutex<Context>>,
225    idle_timeout: u32,
226    js_sender: Arc<Mutex<mpsc::Sender<JsMessage>>>,
227}
228
229impl std::fmt::Debug for IdleManager {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        f.debug_struct("IdleManager")
232            .field("idle_timeout", &self.idle_timeout)
233            .field("callbacks", &{
234                if let Ok(context) = self.context.lock() {
235                    if let Ok(callbacks) = context.callbacks.lock() {
236                        callbacks.len()
237                    } else {
238                        0
239                    }
240                } else {
241                    0
242                }
243            })
244            .field("js_sender", &"<mpsc channel>")
245            .finish()
246    }
247}
248
249impl Drop for IdleManager {
250    fn drop(&mut self) {
251        if let Ok(sender) = self.js_sender.lock() {
252            let _ = sender.send(JsMessage::Cleanup);
253        }
254    }
255}
256
257impl IdleManager {
258    /// Default idle timeout duration in milliseconds (10 minutes).
259    const DEFAULT_IDLE_TIMEOUT: u32 = 10 * 60 * 1000;
260    /// Default scroll debounce duration in milliseconds.
261    const DEFAULT_SCROLL_DEBOUNCE: u32 = 100;
262
263    /// Constructs a new [`IdleManager`] with the given options.
264    pub fn new(options: Option<IdleManagerOptions>) -> Self {
265        let callbacks = options
266            .as_ref()
267            .map(|options| options.on_idle.clone())
268            .unwrap_or_else(|| Arc::new(Mutex::new(Vec::new())));
269
270        let idle_timeout = options
271            .as_ref()
272            .and_then(|options| options.idle_timeout)
273            .unwrap_or(Self::DEFAULT_IDLE_TIMEOUT);
274
275        let (sender, receiver) = mpsc::channel();
276        let js_sender = Arc::new(Mutex::new(sender.clone()));
277
278        // Start the JS handler in a separate task
279        let handler_receiver = receiver;
280        let handler_sender = sender;
281        spawn_local(async move {
282            let mut handler = JsHandler::new(handler_receiver, handler_sender);
283            handler.run().await;
284        });
285
286        let instance = Self {
287            context: Arc::new(Mutex::new(Context { callbacks })),
288            idle_timeout,
289            js_sender,
290        };
291
292        instance.initialize_event_listeners(&options);
293        instance.reset_timer();
294        instance
295    }
296
297    fn initialize_event_listeners(&self, options: &Option<IdleManagerOptions>) {
298        // Create a new JS context just for initializing the event listeners
299        let mut js_context = JsContext::new();
300
301        if js_context.is_initialized {
302            return;
303        }
304
305        for event_type in EVENTS.iter() {
306            let sender = self.js_sender.clone();
307            let callback = Closure::wrap(Box::new(move |_: Event| {
308                if let Ok(sender) = sender.lock() {
309                    let _ = sender.send(JsMessage::ResetTimer(0));
310                }
311            }) as Box<dyn FnMut(Event)>);
312
313            js_context.add_event_listener(event_type, callback);
314        }
315
316        if let Some(true) = options.as_ref().and_then(|options| options.capture_scroll) {
317            let sender = self.js_sender.clone();
318            let scroll_debounce = options
319                .as_ref()
320                .and_then(|options| options.scroll_debounce)
321                .unwrap_or(Self::DEFAULT_SCROLL_DEBOUNCE);
322
323            let callback = Closure::wrap(Box::new(move |_: Event| {
324                if let Ok(sender) = sender.lock() {
325                    let _ = sender.send(JsMessage::ScrollDebounce(scroll_debounce));
326                }
327            }) as Box<dyn FnMut(Event)>);
328
329            js_context.add_event_listener("scroll", callback);
330        }
331
332        js_context.is_initialized = true;
333
334        // The JS context will be dropped after this function, but the event listeners remain
335        // They will be cleaned up when JsHandler processes the Cleanup message
336    }
337
338    /// Registers a callback to be executed when the system becomes idle.
339    pub fn register_callback<F>(&self, callback: F)
340    where
341        F: FnMut() + Send + 'static,
342    {
343        if let Ok(context) = self.context.lock() {
344            if let Ok(mut callbacks) = context.callbacks.lock() {
345                callbacks.push(Box::new(callback));
346            }
347        }
348    }
349
350    /// Exits the idle state, cancels any timeouts, removes event listeners, and executes all registered callbacks.
351    pub fn exit(&mut self) {
352        // Send cleanup message to JS handler
353        if let Ok(sender) = self.js_sender.lock() {
354            let _ = sender.send(JsMessage::Cleanup);
355        }
356
357        // Execute callbacks
358        if let Ok(context) = self.context.lock() {
359            if let Ok(mut callbacks) = context.callbacks.lock() {
360                for callback in callbacks.iter_mut() {
361                    (callback)();
362                }
363            }
364        }
365    }
366
367    /// Resets the idle timer, cancelling any existing timeout and setting a new one.
368    fn reset_timer(&self) {
369        if let Ok(sender) = self.js_sender.lock() {
370            let _ = sender.send(JsMessage::ResetTimer(self.idle_timeout));
371        }
372    }
373}
374
375/// IdleManagerOptions is a struct that contains options for configuring an [`IdleManager`].
376#[derive(Default, Clone)]
377pub struct IdleManagerOptions {
378    /// A callback function to be executed when the system becomes idle.
379    pub on_idle: Arc<Mutex<Vec<Callback>>>,
380    /// The duration of inactivity after which the system is considered idle.
381    pub idle_timeout: Option<u32>,
382    /// A flag indicating whether to capture scroll events.
383    pub capture_scroll: Option<bool>,
384    /// A delay for debouncing scroll events.
385    pub scroll_debounce: Option<u32>,
386}
387
388impl std::fmt::Debug for IdleManagerOptions {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390        let callback_count = if let Ok(callbacks) = self.on_idle.lock() {
391            callbacks.len()
392        } else {
393            0
394        };
395        f.debug_struct("IdleManagerOptions")
396            .field("on_idle", &format!("{} callbacks", callback_count))
397            .field("idle_timeout", &self.idle_timeout)
398            .field("capture_scroll", &self.capture_scroll)
399            .field("scroll_debounce", &self.scroll_debounce)
400            .finish()
401    }
402}
403
404impl IdleManagerOptions {
405    /// Returns a new `IdleManagerOptionsBuilder` to construct an `IdleManagerOptions` struct.
406    pub fn builder() -> IdleManagerOptionsBuilder {
407        IdleManagerOptionsBuilder::default()
408    }
409}
410
411/// Builder for the [`IdleManagerOptions`].
412#[derive(Default)]
413pub struct IdleManagerOptionsBuilder {
414    on_idle: Vec<Callback>,
415    idle_timeout: Option<u32>,
416    capture_scroll: Option<bool>,
417    scroll_debounce: Option<u32>,
418}
419
420impl IdleManagerOptionsBuilder {
421    /// A callback function to be executed when the system becomes idle.
422    pub fn on_idle(&mut self, on_idle: fn()) -> &mut Self {
423        self.on_idle
424            .push(Box::new(on_idle) as Box<dyn FnMut() + Send>);
425        self
426    }
427
428    /// The duration of inactivity after which the system is considered idle.
429    pub fn idle_timeout(&mut self, idle_timeout: u32) -> &mut Self {
430        self.idle_timeout = Some(idle_timeout);
431        self
432    }
433
434    /// A flag indicating whether to capture scroll events.
435    pub fn capture_scroll(&mut self, capture_scroll: bool) -> &mut Self {
436        self.capture_scroll = Some(capture_scroll);
437        self
438    }
439
440    /// A delay for debouncing scroll events.
441    pub fn scroll_debounce(&mut self, scroll_debounce: u32) -> &mut Self {
442        self.scroll_debounce = Some(scroll_debounce);
443        self
444    }
445
446    /// Builds the [`IdleManagerOptions`] struct.
447    pub fn build(&mut self) -> IdleManagerOptions {
448        IdleManagerOptions {
449            on_idle: Arc::new(Mutex::new(mem::take(&mut self.on_idle))),
450            idle_timeout: self.idle_timeout,
451            capture_scroll: self.capture_scroll,
452            scroll_debounce: self.scroll_debounce,
453        }
454    }
455}
456
457#[allow(dead_code)]
458#[cfg(test)]
459mod tests {
460    use super::*;
461    use crate::util::sleep::sleep;
462    use wasm_bindgen_test::*;
463
464    wasm_bindgen_test_configure!(run_in_browser);
465
466    #[wasm_bindgen_test]
467    async fn test_idle_manager() {
468        let options = IdleManagerOptions::builder().idle_timeout(500).build();
469
470        let idle_manager = IdleManager::new(Some(options));
471
472        let callback = Arc::new(Mutex::new(false));
473        let callback_clone = callback.clone();
474        idle_manager.register_callback(move || {
475            *callback_clone.lock().unwrap() = true;
476        });
477
478        assert!(!*callback.lock().unwrap());
479
480        // Wait for the idle timeout to trigger
481        sleep(2000).await;
482
483        assert!(*callback.lock().unwrap());
484    }
485
486    #[wasm_bindgen_test]
487    async fn test_idle_manager_with_reset_timer() {
488        let options = IdleManagerOptions::builder().idle_timeout(1000).build();
489
490        let idle_manager = IdleManager::new(Some(options));
491
492        let callback = Arc::new(Mutex::new(false));
493        let callback_clone = callback.clone();
494        idle_manager.register_callback(move || {
495            *callback_clone.lock().unwrap() = true;
496        });
497
498        assert!(!*callback.lock().unwrap());
499
500        sleep(500).await;
501
502        // Trigger a mousemove event
503        let window = web_sys::window().unwrap();
504        let event = window.document().unwrap().create_event("Event").unwrap();
505        event.init_event("mousemove");
506        window.dispatch_event(&event).unwrap();
507
508        sleep(700).await;
509
510        assert!(!*callback.lock().unwrap());
511
512        // Wait for the idle timeout to trigger
513        sleep(500).await;
514
515        assert!(*callback.lock().unwrap());
516    }
517
518    #[wasm_bindgen_test]
519    async fn test_idle_manager_with_scroll_debounce_1() {
520        let options = IdleManagerOptions::builder()
521            .idle_timeout(1000)
522            .capture_scroll(true)
523            .scroll_debounce(500)
524            .build();
525
526        let idle_manager = IdleManager::new(Some(options));
527
528        let callback = Arc::new(Mutex::new(false));
529        let callback_clone = callback.clone();
530        idle_manager.register_callback(move || {
531            *callback_clone.lock().unwrap() = true;
532        });
533
534        assert!(!*callback.lock().unwrap());
535
536        let window = web_sys::window().unwrap();
537        let event = window.document().unwrap().create_event("Event").unwrap();
538        event.init_event("scroll");
539
540        for _ in 0..7 {
541            sleep(200).await;
542            window.dispatch_event(&event).unwrap();
543        }
544
545        assert!(*callback.lock().unwrap());
546    }
547
548    #[wasm_bindgen_test]
549    async fn test_idle_manager_with_scroll_debounce_2() {
550        let options = IdleManagerOptions::builder()
551            .idle_timeout(1000)
552            .capture_scroll(true)
553            .scroll_debounce(500)
554            .build();
555
556        let idle_manager = IdleManager::new(Some(options));
557
558        let callback = Arc::new(Mutex::new(false));
559        let callback_clone = callback.clone();
560        idle_manager.register_callback(move || {
561            *callback_clone.lock().unwrap() = true;
562        });
563
564        let window = web_sys::window().unwrap();
565        let event = window.document().unwrap().create_event("Event").unwrap();
566        event.init_event("scroll");
567        window.dispatch_event(&event).unwrap();
568
569        assert!(!*callback.lock().unwrap());
570
571        sleep(1200).await;
572
573        assert!(!*callback.lock().unwrap());
574
575        sleep(700).await;
576
577        assert!(*callback.lock().unwrap());
578    }
579}