ic_auth_client/
idle_manager.rs

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