diff --git a/.changes/nested-events.md b/.changes/nested-events.md new file mode 100644 index 000000000..4c2b93655 --- /dev/null +++ b/.changes/nested-events.md @@ -0,0 +1,9 @@ +--- +"tauri": patch +--- + +Window and global events can now be nested inside event handlers. They will run as soon +as the event handler closure is finished in the order they were called. Previously, calling +events inside an event handler would produce a deadlock. + +Note: The order that event handlers are called when triggered is still non-deterministic. diff --git a/core/tauri/src/event.rs b/core/tauri/src/event.rs index 77c8c6878..d1ef2d27a 100644 --- a/core/tauri/src/event.rs +++ b/core/tauri/src/event.rs @@ -41,137 +41,177 @@ impl Event { } } -/// What happens after the handler is called? -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum AfterHandle { - /// The handler is removed (once). - Remove, - - /// Nothing is done (regular). - DoNothing, +/// What to do with the pending handler when resolving it? +enum Pending { + Unlisten(EventHandler), + Listen(EventHandler, Event, Handler), + Trigger(Event, Option, Option), } /// Stored in [`Listeners`] to be called upon when the event that stored it is triggered. struct Handler { window: Option, - callback: Box AfterHandle + Send>, + callback: Box, } /// A collection of handlers. Multiple handlers can represent the same event. type Handlers = HashMap>>; -#[derive(Clone)] -pub(crate) struct Listeners { - inner: Arc>>, +/// Holds event handlers and pending event handlers, along with the salts associating them. +struct InnerListeners { + handlers: Mutex>, + pending: Mutex>>, function_name: Uuid, listeners_object_name: Uuid, queue_object_name: Uuid, } -impl Default for Listeners { +/// A self-contained event manager. +pub(crate) struct Listeners { + inner: Arc>, +} + +impl Default for Listeners { fn default() -> Self { Self { - inner: Arc::new(Mutex::default()), - function_name: Uuid::new_v4(), - listeners_object_name: Uuid::new_v4(), - queue_object_name: Uuid::new_v4(), + inner: Arc::new(InnerListeners { + handlers: Mutex::default(), + pending: Mutex::default(), + function_name: Uuid::new_v4(), + listeners_object_name: Uuid::new_v4(), + queue_object_name: Uuid::new_v4(), + }), } } } -impl Listeners { +impl Clone for Listeners { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Listeners { /// Randomly generated function name to represent the JavaScript event function. pub(crate) fn function_name(&self) -> String { - self.function_name.to_string() + self.inner.function_name.to_string() } /// Randomly generated listener object name to represent the JavaScript event listener object. pub(crate) fn listeners_object_name(&self) -> String { - self.function_name.to_string() + self.inner.listeners_object_name.to_string() } /// Randomly generated queue object name to represent the JavaScript event queue object. pub(crate) fn queue_object_name(&self) -> String { - self.queue_object_name.to_string() + self.inner.queue_object_name.to_string() } - fn listen_internal(&self, event: E, window: Option, handler: F) -> EventHandler - where - F: Fn(Event) -> AfterHandle + Send + 'static, - { - let id = EventHandler(Uuid::new_v4()); - + /// Insert a pending event action to the queue. + fn insert_pending(&self, action: Pending) { self .inner + .pending .lock() - .expect("poisoned event mutex") - .entry(event) - .or_default() - .insert( - id, - Handler { - window, - callback: Box::new(handler), - }, - ); + .expect("poisoned pending event queue") + .push(action) + } + + /// Finish all pending event actions. + fn flush_pending(&self) { + let pending = { + let mut lock = self + .inner + .pending + .lock() + .expect("poisoned pending event queue"); + std::mem::take(&mut *lock) + }; + + for action in pending { + match action { + Pending::Unlisten(id) => self.unlisten(id), + Pending::Listen(id, event, handler) => self.listen_(id, event, handler), + Pending::Trigger(event, window, payload) => self.trigger(event, window, payload), + } + } + } + + fn listen_(&self, id: EventHandler, event: Event, handler: Handler) { + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Listen(id, event, handler)), + Ok(mut lock) => { + lock.entry(event).or_default().insert(id, handler); + } + } + } + + /// Adds an event listener for JS events. + pub(crate) fn listen( + &self, + event: Event, + window: Option, + handler: F, + ) -> EventHandler { + let id = EventHandler(Uuid::new_v4()); + let handler = Handler { + window, + callback: Box::new(handler), + }; + + self.listen_(id, event, handler); id } - /// Adds an event listener for JS events. - pub(crate) fn listen(&self, event: E, window: Option, handler: F) -> EventHandler - where - F: Fn(Event) + Send + 'static, - { - self.listen_internal(event, window, move |event| { - handler(event); - AfterHandle::DoNothing - }) - } - /// Listen to a JS event and immediately unlisten. - pub(crate) fn once( + pub(crate) fn once( &self, - event: E, - window: Option, + event: Event, + window: Option, handler: F, ) -> EventHandler { - self.listen_internal(event, window, move |event| { + let self_ = self.clone(); + self.listen(event, window, move |event| { + self_.unlisten(event.id); handler(event); - AfterHandle::Remove }) } /// Removes an event listener. pub(crate) fn unlisten(&self, handler_id: EventHandler) { - self - .inner - .lock() - .expect("poisoned event mutex") - .values_mut() - .for_each(|handler| { + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Unlisten(handler_id)), + Ok(mut lock) => lock.values_mut().for_each(|handler| { handler.remove(&handler_id); - }) + }), + } } /// Triggers the given global event with its payload. - pub(crate) fn trigger(&self, event: E, window: Option, data: Option) { - if let Some(handlers) = self - .inner - .lock() - .expect("poisoned event mutex") - .get_mut(&event) - { - handlers.retain(|&id, handler| { - if window.is_none() || window == handler.window { - let data = data.clone(); - let payload = Event { id, data }; - (handler.callback)(payload) != AfterHandle::Remove - } else { - // skip and retain all handlers specifying a different window - true + pub(crate) fn trigger(&self, event: Event, window: Option, payload: Option) { + let mut maybe_pending = false; + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Trigger(event, window, payload)), + Ok(lock) => { + if let Some(handlers) = lock.get(&event) { + for (&id, handler) in handlers { + if window.is_none() || window == handler.window { + maybe_pending = true; + (handler.callback)(self::Event { + id, + data: payload.clone(), + }) + } + } } - }) + } + } + + if maybe_pending { + self.flush_pending(); } } } @@ -199,7 +239,7 @@ mod test { listeners.listen(e, None, event_fn); // lock mutex - let l = listeners.inner.lock().unwrap(); + let l = listeners.inner.handlers.lock().unwrap(); // check if the generated key is in the map assert_eq!(l.contains_key(&key), true); @@ -215,7 +255,7 @@ mod test { listeners.listen(e, None, event_fn); // lock mutex - let mut l = listeners.inner.lock().unwrap(); + let mut l = listeners.inner.handlers.lock().unwrap(); // check if l contains key if l.contains_key(&key) { @@ -243,7 +283,7 @@ mod test { listeners.trigger(e, None, Some(d)); // lock the mutex - let l = listeners.inner.lock().unwrap(); + let l = listeners.inner.handlers.lock().unwrap(); // assert that the key is contained in the listeners map assert!(l.contains_key(&key)); diff --git a/core/tauri/src/runtime/manager.rs b/core/tauri/src/runtime/manager.rs index 937c678d7..9bfcd9231 100644 --- a/core/tauri/src/runtime/manager.rs +++ b/core/tauri/src/runtime/manager.rs @@ -31,19 +31,19 @@ use std::{ }; use uuid::Uuid; -pub struct InnerWindowManager { - windows: Mutex>>, - plugins: Mutex>, - listeners: Listeners, +pub struct InnerWindowManager { + windows: Mutex>>, + plugins: Mutex>, + listeners: Listeners, /// The JS message handler. - invoke_handler: Box>, + invoke_handler: Box>, /// The page load hook, invoked when the webview performs a navigation. - on_page_load: Box>, + on_page_load: Box>, config: Config, - assets: Arc, + assets: Arc, default_window_icon: Option>, /// A list of salts that are valid for the current application.