use std::{mem, sync::{Arc, Mutex}};
use wasm_bindgen::{prelude::*, closure::Closure};
use web_sys::{Window, Event};
use std::sync::mpsc;
use wasm_bindgen_futures::spawn_local;
type Callback = Box<dyn FnMut() + Send>;
type JsCallback = Closure<dyn FnMut(Event)>;
const EVENTS: [&str; 6] = ["load", "mousedown", "mousemove", "keydown", "touchstart", "wheel"];
struct JsContext {
event_handlers: Vec<(String, JsCallback)>,
window: Window,
is_initialized: bool,
}
impl JsContext {
fn new() -> Self {
Self {
event_handlers: Vec::new(),
window: web_sys::window().expect("should have a Window"),
is_initialized: false,
}
}
fn add_event_listener(&mut self, event_type: &str, callback: JsCallback) {
self.window
.add_event_listener_with_callback(event_type, callback.as_ref().unchecked_ref())
.expect("should add event listener");
self.event_handlers.push((event_type.to_string(), callback));
}
fn remove_all_listeners(&mut self) {
for (event_type, handler) in self.event_handlers.drain(..) {
self.window
.remove_event_listener_with_callback(
&event_type,
handler.as_ref().unchecked_ref()
)
.expect("should remove event listener");
}
}
fn clear_timeout(&self, timeout_id: i32) {
self.window.clear_timeout_with_handle(timeout_id);
}
fn set_timeout(&self, closure: &Closure<dyn FnMut()>, timeout: i32) -> Result<i32, JsValue> {
self.window.set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
timeout,
)
}
}
impl Drop for JsContext {
fn drop(&mut self) {
self.remove_all_listeners();
}
}
#[derive(Default)]
struct Context {
callbacks: Arc<Mutex<Vec<Callback>>>,
}
enum JsMessage {
ResetTimer(u32),
Cleanup,
ScrollDebounce(u32),
}
struct JsHandler {
context: JsContext,
receiver: mpsc::Receiver<JsMessage>,
sender: mpsc::Sender<JsMessage>,
current_timer: Option<i32>,
current_scroll_debounce_timer: Option<i32>,
exit_closure: Option<Closure<dyn FnMut()>>,
reset_closure: Option<Closure<dyn FnMut()>>,
}
impl JsHandler {
fn new(receiver: mpsc::Receiver<JsMessage>, sender: mpsc::Sender<JsMessage>) -> Self {
Self {
context: JsContext::new(),
receiver,
sender,
current_timer: None,
current_scroll_debounce_timer: None,
exit_closure: None,
reset_closure: None,
}
}
fn get_handler_sender(&self) -> mpsc::Sender<JsMessage> {
self.sender.clone()
}
async fn run(&mut self) {
while let Ok(msg) = self.receiver.recv() {
match msg {
JsMessage::ResetTimer(timeout) => self.handle_reset_timer(timeout),
JsMessage::Cleanup => self.handle_cleanup(),
JsMessage::ScrollDebounce(delay) => self.handle_scroll_debounce(delay),
}
}
}
fn handle_reset_timer(&mut self, timeout: u32) {
if let Some(timer_id) = self.current_timer.take() {
self.context.clear_timeout(timer_id);
}
if timeout == 0 {
return;
}
let (sender, oneshot_receiver) = mpsc::channel();
let exit_closure = Closure::once(move || {
let _ = sender.send(());
});
match self.context.set_timeout(&exit_closure, timeout as i32) {
Ok(timer_id) => {
self.current_timer = Some(timer_id);
self.exit_closure = Some(exit_closure);
let sender = self.get_handler_sender();
spawn_local(async move {
let _ = oneshot_receiver.recv();
let _ = sender.send(JsMessage::Cleanup);
});
},
Err(_) => {
drop(exit_closure);
}
}
}
fn handle_cleanup(&mut self) {
if let Some(timer_id) = self.current_timer.take() {
self.context.clear_timeout(timer_id);
}
if let Some(timer_id) = self.current_scroll_debounce_timer.take() {
self.context.clear_timeout(timer_id);
}
self.context.remove_all_listeners();
self.exit_closure = None;
self.reset_closure = None;
}
fn handle_scroll_debounce(&mut self, delay: u32) {
if let Some(timer_id) = self.current_scroll_debounce_timer.take() {
self.context.clear_timeout(timer_id);
}
let (sender, oneshot_receiver) = mpsc::channel();
let reset_closure = Closure::once(move || {
let _ = sender.send(());
});
match self.context.set_timeout(&reset_closure, delay as i32) {
Ok(timer_id) => {
self.current_scroll_debounce_timer = Some(timer_id);
self.reset_closure = Some(reset_closure);
let sender = self.get_handler_sender();
spawn_local(async move {
let _ = oneshot_receiver.recv();
let _ = sender.send(JsMessage::ResetTimer(0));
});
},
Err(_) => {
drop(reset_closure);
}
}
}
}
#[derive(Clone)]
pub struct IdleManager {
context: Arc<Mutex<Context>>,
idle_timeout: u32,
js_sender: Arc<Mutex<mpsc::Sender<JsMessage>>>,
}
impl std::fmt::Debug for IdleManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IdleManager")
.field("idle_timeout", &self.idle_timeout)
.field("callbacks", &{
if let Ok(context) = self.context.lock() {
if let Ok(callbacks) = context.callbacks.lock() {
callbacks.len()
} else {
0
}
} else {
0
}
})
.field("js_sender", &"<mpsc channel>")
.finish()
}
}
impl Drop for IdleManager {
fn drop(&mut self) {
if let Ok(sender) = self.js_sender.lock() {
let _ = sender.send(JsMessage::Cleanup);
}
}
}
impl IdleManager {
const DEFAULT_IDLE_TIMEOUT: u32 = 10 * 60 * 1000;
const DEFAULT_SCROLL_DEBOUNCE: u32 = 100;
pub fn new(options: Option<IdleManagerOptions>) -> Self {
let callbacks = options
.as_ref()
.map(|options| options.on_idle.clone())
.unwrap_or_else(|| Arc::new(Mutex::new(Vec::new())));
let idle_timeout = options
.as_ref()
.and_then(|options| options.idle_timeout)
.unwrap_or(Self::DEFAULT_IDLE_TIMEOUT);
let (sender, receiver) = mpsc::channel();
let js_sender = Arc::new(Mutex::new(sender.clone()));
let handler_receiver = receiver;
let handler_sender = sender;
spawn_local(async move {
let mut handler = JsHandler::new(handler_receiver, handler_sender);
handler.run().await;
});
let instance = Self {
context: Arc::new(Mutex::new(
Context {
callbacks,
}
)),
idle_timeout,
js_sender,
};
instance.initialize_event_listeners(&options);
instance.reset_timer();
instance
}
fn initialize_event_listeners(&self, options: &Option<IdleManagerOptions>) {
let mut js_context = JsContext::new();
if js_context.is_initialized {
return;
}
for event_type in EVENTS.iter() {
let sender = self.js_sender.clone();
let callback = Closure::wrap(Box::new(move |_: Event| {
if let Ok(sender) = sender.lock() {
let _ = sender.send(JsMessage::ResetTimer(0));
}
}) as Box<dyn FnMut(Event)>);
js_context.add_event_listener(event_type, callback);
}
if let Some(true) = options.as_ref().and_then(|options| options.capture_scroll) {
let sender = self.js_sender.clone();
let scroll_debounce = options.as_ref().and_then(|options| options.scroll_debounce)
.unwrap_or(Self::DEFAULT_SCROLL_DEBOUNCE);
let callback = Closure::wrap(Box::new(move |_: Event| {
if let Ok(sender) = sender.lock() {
let _ = sender.send(JsMessage::ScrollDebounce(scroll_debounce));
}
}) as Box<dyn FnMut(Event)>);
js_context.add_event_listener("scroll", callback);
}
js_context.is_initialized = true;
}
pub fn register_callback<F>(&self, callback: F)
where
F: FnMut() + Send + 'static,
{
self.context.lock().unwrap().callbacks.lock().unwrap().push(Box::new(callback));
}
pub fn exit(&mut self) {
if let Ok(sender) = self.js_sender.lock() {
let _ = sender.send(JsMessage::Cleanup);
}
let context = self.context.lock().unwrap();
for callback in context.callbacks.lock().unwrap().iter_mut() {
(callback)();
}
}
fn reset_timer(&self) {
if let Ok(sender) = self.js_sender.lock() {
let _ = sender.send(JsMessage::ResetTimer(self.idle_timeout));
}
}
}
#[derive(Default, Clone)]
pub struct IdleManagerOptions {
pub on_idle: Arc<Mutex<Vec<Callback>>>,
pub idle_timeout: Option<u32>,
pub capture_scroll: Option<bool>,
pub scroll_debounce: Option<u32>,
}
impl std::fmt::Debug for IdleManagerOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let callback_count = self.on_idle.lock().unwrap().len();
f.debug_struct("IdleManagerOptions")
.field("on_idle", &format!("{} callbacks", callback_count))
.field("idle_timeout", &self.idle_timeout)
.field("capture_scroll", &self.capture_scroll)
.field("scroll_debounce", &self.scroll_debounce)
.finish()
}
}
impl IdleManagerOptions {
pub fn builder() -> IdleManagerOptionsBuilder {
IdleManagerOptionsBuilder::default()
}
}
#[derive(Default)]
pub struct IdleManagerOptionsBuilder {
on_idle: Vec<Callback>,
idle_timeout: Option<u32>,
capture_scroll: Option<bool>,
scroll_debounce: Option<u32>,
}
impl IdleManagerOptionsBuilder {
pub fn on_idle(&mut self, on_idle: fn()) -> &mut Self {
self.on_idle.push(Box::new(on_idle) as Box<dyn FnMut() + Send>);
self
}
pub fn idle_timeout(&mut self, idle_timeout: u32) -> &mut Self {
self.idle_timeout = Some(idle_timeout);
self
}
pub fn capture_scroll(&mut self, capture_scroll: bool) -> &mut Self {
self.capture_scroll = Some(capture_scroll);
self
}
pub fn scroll_debounce(&mut self, scroll_debounce: u32) -> &mut Self {
self.scroll_debounce = Some(scroll_debounce);
self
}
pub fn build(&mut self) -> IdleManagerOptions {
IdleManagerOptions {
on_idle: Arc::new(Mutex::new(mem::take(&mut self.on_idle))),
idle_timeout: self.idle_timeout,
capture_scroll: self.capture_scroll,
scroll_debounce: self.scroll_debounce,
}
}
}
#[allow(dead_code)]
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
use crate::util::sleep::sleep;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn test_idle_manager() {
let options = IdleManagerOptions::builder()
.idle_timeout(500)
.build();
let idle_manager = IdleManager::new(Some(options));
let callback = Arc::new(Mutex::new(false));
let callback_clone = callback.clone();
idle_manager.register_callback(move || {
*callback_clone.lock().unwrap() = true;
});
assert!(!*callback.lock().unwrap());
sleep(2000).await;
assert!(*callback.lock().unwrap());
}
#[wasm_bindgen_test]
async fn test_idle_manager_with_reset_timer() {
let options = IdleManagerOptions::builder()
.idle_timeout(1000)
.build();
let idle_manager = IdleManager::new(Some(options));
let callback = Arc::new(Mutex::new(false));
let callback_clone = callback.clone();
idle_manager.register_callback(move || {
*callback_clone.lock().unwrap() = true;
});
assert!(!*callback.lock().unwrap());
sleep(500).await;
let window = web_sys::window().unwrap();
let event = window.document().unwrap().create_event("Event").unwrap();
event.init_event("mousemove");
window.dispatch_event(&event).unwrap();
sleep(700).await;
assert!(!*callback.lock().unwrap());
sleep(500).await;
assert!(*callback.lock().unwrap());
}
#[wasm_bindgen_test]
async fn test_idle_manager_with_scroll_debounce_1() {
let options = IdleManagerOptions::builder()
.idle_timeout(1000)
.capture_scroll(true)
.scroll_debounce(500)
.build();
let idle_manager = IdleManager::new(Some(options));
let callback = Arc::new(Mutex::new(false));
let callback_clone = callback.clone();
idle_manager.register_callback(move || {
*callback_clone.lock().unwrap() = true;
});
assert!(!*callback.lock().unwrap());
let window = web_sys::window().unwrap();
let event = window.document().unwrap().create_event("Event").unwrap();
event.init_event("scroll");
for _ in 0..7 {
sleep(200).await;
window.dispatch_event(&event).unwrap();
}
assert!(*callback.lock().unwrap());
}
#[wasm_bindgen_test]
async fn test_idle_manager_with_scroll_debounce_2() {
let options = IdleManagerOptions::builder()
.idle_timeout(1000)
.capture_scroll(true)
.scroll_debounce(500)
.build();
let idle_manager = IdleManager::new(Some(options));
let callback = Arc::new(Mutex::new(false));
let callback_clone = callback.clone();
idle_manager.register_callback(move || {
*callback_clone.lock().unwrap() = true;
});
let window = web_sys::window().unwrap();
let event = window.document().unwrap().create_event("Event").unwrap();
event.init_event("scroll");
window.dispatch_event(&event).unwrap();
assert!(!*callback.lock().unwrap());
sleep(1200).await;
assert!(!*callback.lock().unwrap());
sleep(700).await;
assert!(*callback.lock().unwrap());
}
}