1use futures::channel::{mpsc, oneshot};
2use gloo_utils::window;
3use std::{
4 mem,
5 sync::{Arc, Mutex},
6};
7use wasm_bindgen::{closure::Closure, prelude::*};
8use wasm_bindgen_futures::spawn_local;
9use web_sys::{Event, Window};
10
11type Callback = Box<dyn FnMut() + Send>;
12type JsCallback = Closure<dyn FnMut(Event)>;
13
14const EVENTS: [&str; 6] = [
15 "load",
16 "mousedown",
17 "mousemove",
18 "keydown",
19 "touchstart",
20 "wheel",
21];
22
23struct JsContext {
25 event_handlers: Vec<(String, JsCallback)>,
26 window: Window,
27}
28
29impl JsContext {
30 fn new() -> Self {
31 Self {
32 event_handlers: Vec::new(),
33 window: window(),
34 }
35 }
36
37 fn add_event_listener(&mut self, event_type: &str, callback: JsCallback) {
38 self.window
39 .add_event_listener_with_callback(event_type, callback.as_ref().unchecked_ref())
40 .expect("should add event listener");
41 self.event_handlers.push((event_type.to_string(), callback));
42 }
43
44 fn remove_all_listeners(&mut self) {
45 for (event_type, handler) in self.event_handlers.drain(..) {
46 self.window
47 .remove_event_listener_with_callback(&event_type, handler.as_ref().unchecked_ref())
48 .expect("should remove event listener");
49 }
50 }
51
52 fn clear_timeout(&self, timeout_id: i32) {
53 self.window.clear_timeout_with_handle(timeout_id);
54 }
55
56 fn set_timeout(&self, closure: &Closure<dyn FnMut()>, timeout: i32) -> Result<i32, JsValue> {
57 self.window
58 .set_timeout_with_callback_and_timeout_and_arguments_0(
59 closure.as_ref().unchecked_ref(),
60 timeout,
61 )
62 }
63}
64
65impl Drop for JsContext {
66 fn drop(&mut self) {
67 self.remove_all_listeners();
68 }
69}
70
71#[derive(Default)]
73struct Context {
74 callbacks: Arc<Mutex<Vec<Callback>>>,
75}
76
77enum JsMessage {
78 ResetTimer(u32),
79 Cleanup,
80 CleanupWithCallbacks,
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 callbacks: Arc<Mutex<Vec<Callback>>>,
93 idle_timeout: u32,
94}
95
96impl JsHandler {
97 fn new(
98 receiver: mpsc::Receiver<JsMessage>,
99 sender: mpsc::Sender<JsMessage>,
100 callbacks: Arc<Mutex<Vec<Callback>>>,
101 idle_timeout: u32,
102 ) -> Self {
103 Self {
104 context: JsContext::new(),
105 receiver,
106 sender,
107 current_timer: None,
108 current_scroll_debounce_timer: None,
109 exit_closure: None,
110 reset_closure: None,
111 callbacks,
112 idle_timeout,
113 }
114 }
115
116 fn get_handler_sender(&self) -> mpsc::Sender<JsMessage> {
117 self.sender.clone()
118 }
119
120 async fn run(&mut self) {
121 use futures::StreamExt;
122 while let Some(msg) = self.receiver.next().await {
123 match msg {
124 JsMessage::ResetTimer(timeout) => self.handle_reset_timer(timeout),
125 JsMessage::Cleanup => self.handle_cleanup(false),
126 JsMessage::CleanupWithCallbacks => self.handle_cleanup(true),
127 JsMessage::ScrollDebounce(delay) => self.handle_scroll_debounce(delay),
128 }
129 }
130 }
131
132 fn handle_reset_timer(&mut self, timeout: u32) {
133 if let Some(timer_id) = self.current_timer.take() {
135 self.context.clear_timeout(timer_id);
136 }
137
138 let actual_timeout = if timeout == 0 {
139 self.idle_timeout } else {
141 timeout
142 };
143
144 if actual_timeout == 0 {
146 return;
147 }
148
149 let (sender, oneshot_receiver) = oneshot::channel();
151
152 let exit_closure = Closure::once(move || {
154 let _ = sender.send(());
156 });
157
158 match self
160 .context
161 .set_timeout(&exit_closure, actual_timeout as i32)
162 {
163 Ok(timer_id) => {
164 self.current_timer = Some(timer_id);
165 self.exit_closure = Some(exit_closure);
166
167 let mut sender = self.get_handler_sender();
169 spawn_local(async move {
170 if oneshot_receiver.await.is_ok() {
172 use futures::SinkExt;
174 let _ = sender.send(JsMessage::CleanupWithCallbacks).await;
175 }
176 });
177 }
178 Err(_) => {
179 drop(exit_closure);
181 }
182 }
183 }
184
185 fn handle_cleanup(&mut self, execute: bool) {
186 if let Some(timer_id) = self.current_timer.take() {
188 self.context.clear_timeout(timer_id);
189 }
190
191 if let Some(timer_id) = self.current_scroll_debounce_timer.take() {
192 self.context.clear_timeout(timer_id);
193 }
194
195 self.context.remove_all_listeners();
197
198 self.exit_closure = None;
200 self.reset_closure = None;
201
202 if execute && let Ok(mut callbacks) = self.callbacks.lock() {
204 for callback in callbacks.iter_mut() {
205 (callback)();
206 }
207 }
208 }
209
210 fn handle_scroll_debounce(&mut self, delay: u32) {
211 if let Some(timer_id) = self.current_scroll_debounce_timer.take() {
213 self.context.clear_timeout(timer_id);
214 }
215
216 let (sender, oneshot_receiver) = oneshot::channel();
218
219 let reset_closure = Closure::once(move || {
221 let _ = sender.send(());
223 });
224
225 match self.context.set_timeout(&reset_closure, delay as i32) {
227 Ok(timer_id) => {
228 self.current_scroll_debounce_timer = Some(timer_id);
229 self.reset_closure = Some(reset_closure);
230
231 let mut sender = self.get_handler_sender();
233 spawn_local(async move {
234 if oneshot_receiver.await.is_ok() {
236 use futures::SinkExt;
238 let _ = sender.send(JsMessage::ResetTimer(0)).await;
239 }
240 });
241 }
242 Err(_) => {
243 drop(reset_closure);
245 }
246 }
247 }
248}
249
250#[derive(Clone)]
254pub struct IdleManager {
255 context: Arc<Mutex<Context>>,
256 idle_timeout: u32,
257 js_sender: Arc<Mutex<mpsc::Sender<JsMessage>>>,
258 is_initialized: Arc<Mutex<bool>>,
259}
260
261impl std::fmt::Debug for IdleManager {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 f.debug_struct("IdleManager")
264 .field("idle_timeout", &self.idle_timeout)
265 .field("callbacks", &{
266 if let Ok(context) = self.context.lock() {
267 if let Ok(callbacks) = context.callbacks.lock() {
268 callbacks.len()
269 } else {
270 0
271 }
272 } else {
273 0
274 }
275 })
276 .field("js_sender", &"<mpsc channel>")
277 .finish()
278 }
279}
280
281impl Drop for IdleManager {
282 fn drop(&mut self) {
283 use futures::SinkExt;
284 let mut sender_clone = self.js_sender.lock().unwrap().clone();
285 spawn_local(async move {
286 let _ = sender_clone.send(JsMessage::Cleanup).await;
287 });
288 }
289}
290
291impl IdleManager {
292 const DEFAULT_IDLE_TIMEOUT: u32 = 10 * 60 * 1000;
294 const DEFAULT_SCROLL_DEBOUNCE: u32 = 100;
296
297 pub fn new(options: Option<IdleManagerOptions>) -> Self {
299 let callbacks = options
300 .as_ref()
301 .map(|options| options.on_idle.clone())
302 .unwrap_or_else(|| Arc::new(Mutex::new(Vec::new())));
303
304 let idle_timeout = options
305 .as_ref()
306 .and_then(|options| options.idle_timeout)
307 .unwrap_or(Self::DEFAULT_IDLE_TIMEOUT);
308
309 let (sender, receiver) = mpsc::channel(10);
310 let js_sender = Arc::new(Mutex::new(sender.clone()));
311
312 let handler_receiver = receiver;
314 let handler_sender = sender;
315 let callbacks_for_handler = callbacks.clone();
316 let idle_timeout_for_handler = idle_timeout;
317 spawn_local(async move {
318 let mut handler = JsHandler::new(
319 handler_receiver,
320 handler_sender,
321 callbacks_for_handler,
322 idle_timeout_for_handler,
323 );
324 handler.run().await;
325 });
326
327 let instance = Self {
328 context: Arc::new(Mutex::new(Context { callbacks })),
329 idle_timeout,
330 js_sender,
331 is_initialized: Arc::new(Mutex::new(false)),
332 };
333
334 instance.initialize_event_listeners(&options);
335 instance.reset_timer();
336 instance
337 }
338
339 fn initialize_event_listeners(&self, options: &Option<IdleManagerOptions>) {
340 let mut is_initialized = self.is_initialized.lock().unwrap();
341 if *is_initialized {
342 return;
343 }
344
345 let mut js_context = JsContext::new();
346
347 for event_type in EVENTS.iter() {
348 let sender = self.js_sender.lock().unwrap().clone();
349 let callback = Closure::wrap(Box::new(move |_: Event| {
350 use futures::SinkExt;
351 let mut sender_clone = sender.clone();
352 spawn_local(async move {
353 let _ = sender_clone.send(JsMessage::ResetTimer(0)).await;
354 });
355 }) as Box<dyn FnMut(Event)>);
356
357 js_context.add_event_listener(event_type, callback);
358 }
359
360 if let Some(true) = options.as_ref().and_then(|options| options.capture_scroll) {
361 let sender = self.js_sender.lock().unwrap().clone();
362 let scroll_debounce = options
363 .as_ref()
364 .and_then(|options| options.scroll_debounce)
365 .unwrap_or(Self::DEFAULT_SCROLL_DEBOUNCE);
366
367 let callback = Closure::wrap(Box::new(move |_: Event| {
368 use futures::SinkExt;
369 let mut sender_clone = sender.clone();
370 spawn_local(async move {
371 let _ = sender_clone
372 .send(JsMessage::ScrollDebounce(scroll_debounce))
373 .await;
374 });
375 }) as Box<dyn FnMut(Event)>);
376
377 js_context.add_event_listener("scroll", callback);
378 }
379
380 *is_initialized = true;
381 Box::leak(Box::new(js_context));
382 }
383
384 pub fn register_callback<F>(&self, callback: F)
386 where
387 F: FnMut() + Send + 'static,
388 {
389 if let Ok(context) = self.context.lock() {
390 if let Ok(mut callbacks) = context.callbacks.lock() {
391 callbacks.push(Box::new(callback));
392 }
393 }
394 }
395
396 pub fn exit(&mut self) {
398 use futures::SinkExt;
399 let mut sender_clone = self.js_sender.lock().unwrap().clone();
401 spawn_local(async move {
402 let _ = sender_clone.send(JsMessage::CleanupWithCallbacks).await;
403 });
404
405 }
407 fn reset_timer(&self) {
409 use futures::SinkExt;
410 let mut sender_clone = self.js_sender.lock().unwrap().clone();
411 let timeout = self.idle_timeout;
412 spawn_local(async move {
413 let _ = sender_clone.send(JsMessage::ResetTimer(timeout)).await;
414 });
415 }
416}
417#[derive(Default, Clone)]
419pub struct IdleManagerOptions {
420 pub on_idle: Arc<Mutex<Vec<Callback>>>,
422 pub idle_timeout: Option<u32>,
424 pub capture_scroll: Option<bool>,
426 pub scroll_debounce: Option<u32>,
428}
429
430impl std::fmt::Debug for IdleManagerOptions {
431 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432 let callback_count = if let Ok(callbacks) = self.on_idle.lock() {
433 callbacks.len()
434 } else {
435 0
436 };
437 f.debug_struct("IdleManagerOptions")
438 .field("on_idle", &format!("{} callbacks", callback_count))
439 .field("idle_timeout", &self.idle_timeout)
440 .field("capture_scroll", &self.capture_scroll)
441 .field("scroll_debounce", &self.scroll_debounce)
442 .finish()
443 }
444}
445
446impl IdleManagerOptions {
447 pub fn builder() -> IdleManagerOptionsBuilder {
449 IdleManagerOptionsBuilder::default()
450 }
451}
452
453#[derive(Default)]
455pub struct IdleManagerOptionsBuilder {
456 on_idle: Vec<Callback>,
457 idle_timeout: Option<u32>,
458 capture_scroll: Option<bool>,
459 scroll_debounce: Option<u32>,
460}
461
462impl IdleManagerOptionsBuilder {
463 pub fn on_idle(&mut self, on_idle: fn()) -> &mut Self {
465 self.on_idle
466 .push(Box::new(on_idle) as Box<dyn FnMut() + Send>);
467 self
468 }
469
470 pub fn idle_timeout(&mut self, idle_timeout: u32) -> &mut Self {
472 self.idle_timeout = Some(idle_timeout);
473 self
474 }
475
476 pub fn capture_scroll(&mut self, capture_scroll: bool) -> &mut Self {
478 self.capture_scroll = Some(capture_scroll);
479 self
480 }
481
482 pub fn scroll_debounce(&mut self, scroll_debounce: u32) -> &mut Self {
484 self.scroll_debounce = Some(scroll_debounce);
485 self
486 }
487
488 pub fn build(&mut self) -> IdleManagerOptions {
490 IdleManagerOptions {
491 on_idle: Arc::new(Mutex::new(mem::take(&mut self.on_idle))),
492 idle_timeout: self.idle_timeout,
493 capture_scroll: self.capture_scroll,
494 scroll_debounce: self.scroll_debounce,
495 }
496 }
497}
498
499#[allow(dead_code)]
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::util::sleep::sleep;
504 use wasm_bindgen_test::*;
505
506 wasm_bindgen_test_configure!(run_in_browser);
507
508 #[wasm_bindgen_test]
509 async fn test_idle_manager() {
510 let options = IdleManagerOptions::builder().idle_timeout(500).build();
511
512 let idle_manager = IdleManager::new(Some(options));
513
514 let callback = Arc::new(Mutex::new(false));
515 let callback_clone = callback.clone();
516 idle_manager.register_callback(move || {
517 *callback_clone.lock().unwrap() = true;
518 });
519
520 assert!(!*callback.lock().unwrap());
521
522 sleep(2000).await;
524
525 assert!(*callback.lock().unwrap());
526 }
527
528 #[wasm_bindgen_test]
529 async fn test_idle_manager_with_reset_timer() {
530 let options = IdleManagerOptions::builder().idle_timeout(1000).build();
531
532 let idle_manager = IdleManager::new(Some(options));
533
534 let callback = Arc::new(Mutex::new(false));
535 let callback_clone = callback.clone();
536 idle_manager.register_callback(move || {
537 *callback_clone.lock().unwrap() = true;
538 });
539
540 assert!(!*callback.lock().unwrap());
541
542 sleep(500).await;
543
544 let window = window();
546 let event = window.document().unwrap().create_event("Event").unwrap();
547 event.init_event("mousemove");
548 window.dispatch_event(&event).unwrap();
549
550 sleep(700).await;
551
552 assert!(!*callback.lock().unwrap());
553
554 sleep(500).await;
556
557 assert!(*callback.lock().unwrap());
558 }
559
560 #[wasm_bindgen_test]
561 async fn test_idle_manager_with_scroll_debounce_1() {
562 let options = IdleManagerOptions::builder()
563 .idle_timeout(1000)
564 .capture_scroll(true)
565 .scroll_debounce(500)
566 .build();
567
568 let idle_manager = IdleManager::new(Some(options));
569
570 let callback = Arc::new(Mutex::new(false));
571 let callback_clone = callback.clone();
572 idle_manager.register_callback(move || {
573 *callback_clone.lock().unwrap() = true;
574 });
575
576 assert!(!*callback.lock().unwrap());
577
578 let window = window();
579 let event = window.document().unwrap().create_event("Event").unwrap();
580 event.init_event("scroll");
581
582 for _ in 0..7 {
583 sleep(200).await;
584 window.dispatch_event(&event).unwrap();
585 }
586
587 assert!(*callback.lock().unwrap());
588 }
589
590 #[wasm_bindgen_test]
591 async fn test_idle_manager_with_scroll_debounce_2() {
592 let options = IdleManagerOptions::builder()
593 .idle_timeout(1000)
594 .capture_scroll(true)
595 .scroll_debounce(500)
596 .build();
597
598 let idle_manager = IdleManager::new(Some(options));
599
600 let callback = Arc::new(Mutex::new(false));
601 let callback_clone = callback.clone();
602 idle_manager.register_callback(move || {
603 *callback_clone.lock().unwrap() = true;
604 });
605
606 let window = window();
607 let event = window.document().unwrap().create_event("Event").unwrap();
608 event.init_event("scroll");
609 window.dispatch_event(&event).unwrap();
610
611 assert!(!*callback.lock().unwrap());
612
613 sleep(1200).await;
614
615 assert!(!*callback.lock().unwrap());
616
617 sleep(700).await;
618
619 assert!(*callback.lock().unwrap());
620 }
621}