Przeglądaj źródła

refactor(core): add `unlisten`, `once` APIs to the event system (#1359)

Lucas Fernandes Nogueira 4 lat temu
rodzic
commit
b670ec55f2

+ 6 - 0
.changes/event-unlisten-js.md

@@ -0,0 +1,6 @@
+---
+"tauri-api": minor
+"tauri": minor
+---
+
+Refactor the event callback payload and return an unlisten function on the `listen` API.

+ 5 - 0
.changes/event-unlisten-rust.md

@@ -0,0 +1,5 @@
+---
+"tauri": minor
+---
+
+Adds `unlisten` and `once` APIs on the Rust event system.

+ 36 - 11
api/src/helpers/event.ts

@@ -2,24 +2,45 @@ import { invokeTauriCommand } from './tauri'
 import { transformCallback } from '../tauri'
 
 export interface Event<T> {
-  type: string
+  /// event name.
+  event: string
+  /// event identifier used to unlisten.
+  id: number
+  /// event payload.
   payload: T
 }
 
 export type EventCallback<T> = (event: Event<T>) => void
 
+export type UnlistenFn = () => void
+
 async function _listen<T>(
   event: string,
-  handler: EventCallback<T>,
-  once: boolean
-): Promise<void> {
-  await invokeTauriCommand({
+  handler: EventCallback<T>
+): Promise<UnlistenFn> {
+  return invokeTauriCommand<number>({
     __tauriModule: 'Event',
     message: {
       cmd: 'listen',
       event,
-      handler: transformCallback(handler, once),
-      once
+      handler: transformCallback(handler)
+    }
+  }).then((eventId) => {
+    return async () => _unlisten(eventId)
+  })
+}
+
+/**
+ * Unregister the event listener associated with the given id.
+ *
+ * @param {number} eventId the event identifier
+ */
+async function _unlisten(eventId: number): Promise<void> {
+  return invokeTauriCommand({
+    __tauriModule: 'Event',
+    message: {
+      cmd: 'unlisten',
+      eventId
     }
   })
 }
@@ -29,12 +50,13 @@ async function _listen<T>(
  *
  * @param event the event name
  * @param handler the event handler callback
+ * @return {Promise<UnlistenFn>} a promise resolving to a function to unlisten to the event.
  */
 async function listen<T>(
   event: string,
   handler: EventCallback<T>
-): Promise<void> {
-  return _listen(event, handler, false)
+): Promise<UnlistenFn> {
+  return _listen(event, handler)
 }
 
 /**
@@ -46,8 +68,11 @@ async function listen<T>(
 async function once<T>(
   event: string,
   handler: EventCallback<T>
-): Promise<void> {
-  return _listen(event, handler, true)
+): Promise<UnlistenFn> {
+  return _listen<T>(event, (eventData) => {
+    handler(eventData)
+    _unlisten(eventData.id).catch(() => {})
+  })
 }
 
 /**

+ 8 - 4
api/src/window.ts

@@ -1,5 +1,5 @@
 import { invokeTauriCommand } from './helpers/tauri'
-import { EventCallback, emit, listen, once } from './helpers/event'
+import { EventCallback, UnlistenFn, emit, listen, once } from './helpers/event'
 
 interface WindowDef {
   label: string
@@ -39,10 +39,14 @@ class WebviewWindowHandle {
    *
    * @param event the event name
    * @param handler the event handler callback
+   * @return {Promise<UnlistenFn>} a promise resolving to a function to unlisten to the event.
    */
-  async listen<T>(event: string, handler: EventCallback<T>): Promise<void> {
+  async listen<T>(
+    event: string,
+    handler: EventCallback<T>
+  ): Promise<UnlistenFn> {
     if (this._handleTauriEvent(event, handler)) {
-      return Promise.resolve()
+      return Promise.resolve(() => {})
     }
     return listen(event, handler)
   }
@@ -70,7 +74,7 @@ class WebviewWindowHandle {
     if (localTauriEvents.includes(event)) {
       // eslint-disable-next-line
       for (const handler of this.listeners[event] || []) {
-        handler({ type: event, payload })
+        handler({ event, id: -1, payload })
       }
       return Promise.resolve()
     }

Plik diff jest za duży
+ 0 - 0
examples/api/public/build/bundle.js


Plik diff jest za duży
+ 0 - 0
examples/api/public/build/bundle.js.map


+ 2 - 2
examples/api/src-tauri/src/main.rs

@@ -17,8 +17,8 @@ fn main() {
     .setup(|webview_manager| async move {
       let dispatcher = webview_manager.current_webview().unwrap();
       let dispatcher_ = dispatcher.clone();
-      dispatcher.listen("js-event", move |msg| {
-        println!("got js-event with message '{:?}'", msg);
+      dispatcher.listen("js-event", move |event| {
+        println!("got js-event with message '{:?}'", event.payload());
         let reply = Reply {
           data: "something else".to_string(),
         };

+ 11 - 27
examples/api/src/App.svelte

@@ -61,6 +61,7 @@
 
   function onMessage(value) {
     responses += typeof value === "string" ? value : JSON.stringify(value);
+    responses += "\n";
   }
 
   function onLogoClick() {
@@ -72,25 +73,13 @@
   <div class="flex row noselect just-around" style="margin=1em;">
     <img src="tauri.png" height="60" on:click={onLogoClick} alt="logo" />
     <div>
-      <a
-        class="dark-link"
-        target="_blank"
-        href="https://tauri.studio/en/docs/getting-started/intro"
-      >
+      <a class="dark-link" target="_blank" href="https://tauri.studio/en/docs/getting-started/intro">
         Documentation
       </a>
-      <a
-        class="dark-link"
-        target="_blank"
-        href="https://github.com/tauri-apps/tauri"
-      >
+      <a class="dark-link" target="_blank" href="https://github.com/tauri-apps/tauri">
         Github
       </a>
-      <a
-        class="dark-link"
-        target="_blank"
-        href="https://github.com/tauri-apps/tauri/tree/dev/tauri/examples/api"
-      >
+      <a class="dark-link" target="_blank" href="https://github.com/tauri-apps/tauri/tree/dev/tauri/examples/api">
         Source
       </a>
     </div>
@@ -98,12 +87,10 @@
   <div class="flex row">
     <div style="width:15em; margin-left:0.5em">
       {#each views as view}
-        <p
-          class="nv noselect {selected === view ? 'nv_selected' : ''}"
-          on:click={() => select(view)}
+      <p class="nv noselect {selected === view ? 'nv_selected' : ''}" on:click={()=> select(view)}
         >
-          {view.label}
-        </p>
+        {view.label}
+      </p>
       {/each}
     </div>
     <div class="content">
@@ -113,13 +100,10 @@
   <div id="response">
     <p class="flex row just-around">
       <strong>Tauri Console</strong>
-      <a
-        class="nv"
-        on:click={() => {
-          responses = [""];
-        }}>clear</a
-      >
+      <a class="nv" on:click={()=> {
+        responses = [""];
+        }}>clear</a>
     </p>
     {responses}
   </div>
-</main>
+</main>

+ 1 - 1
examples/api/src/components/Communication.svelte

@@ -4,7 +4,7 @@
 
   export let onMessage;
 
-  listen("rust-event", onMessage);
+  listen("rust-event", onMessage)
 
   function log() {
     invoke("log_operation", {

Plik diff jest za duży
+ 0 - 0
examples/multiwindow/dist/__tauri.js


BIN
tauri/examples/api/public/tauri.png


+ 71 - 11
tauri/src/app/event.rs

@@ -10,12 +10,17 @@ use once_cell::sync::Lazy;
 use serde::Serialize;
 use serde_json::Value as JsonValue;
 
+/// Event identifier.
+pub type EventId = u64;
+
 /// An event handler.
 struct EventHandler {
+  /// Event identifier.
+  id: EventId,
   /// A event handler might be global or tied to a window.
   window_label: Option<String>,
   /// The on event callback.
-  on_event: Box<dyn FnMut(Option<String>) + Send>,
+  on_event: Box<dyn Fn(EventPayload) + Send>,
 }
 
 type Listeners = Arc<Mutex<HashMap<String, Vec<EventHandler>>>>;
@@ -47,24 +52,71 @@ pub fn event_queue_object_name() -> String {
   EVENT_QUEUE_OBJECT_NAME.to_string()
 }
 
+#[derive(Debug, Clone)]
+pub struct EventPayload {
+  id: EventId,
+  payload: Option<String>,
+}
+
+impl EventPayload {
+  /// The event identifier.
+  pub fn id(&self) -> EventId {
+    self.id
+  }
+
+  /// The event payload.
+  pub fn payload(&self) -> Option<&String> {
+    self.payload.as_ref()
+  }
+}
+
 /// Adds an event listener for JS events.
-pub fn listen<F: FnMut(Option<String>) + Send + 'static>(
-  id: impl AsRef<str>,
+pub fn listen<F: Fn(EventPayload) + Send + 'static>(
+  event_name: impl AsRef<str>,
   window_label: Option<String>,
   handler: F,
-) {
+) -> EventId {
   let mut l = listeners()
     .lock()
     .expect("Failed to lock listeners: listen()");
+  let id = rand::random();
   let handler = EventHandler {
+    id,
     window_label,
     on_event: Box::new(handler),
   };
-  if let Some(listeners) = l.get_mut(id.as_ref()) {
+  if let Some(listeners) = l.get_mut(event_name.as_ref()) {
     listeners.push(handler);
   } else {
-    l.insert(id.as_ref().to_string(), vec![handler]);
+    l.insert(event_name.as_ref().to_string(), vec![handler]);
   }
+  id
+}
+
+/// Listen to an JS event and immediately unlisten.
+pub fn once<F: Fn(EventPayload) + Send + 'static>(
+  event_name: impl AsRef<str>,
+  window_label: Option<String>,
+  handler: F,
+) {
+  listen(event_name, window_label, move |event| {
+    unlisten(event.id);
+    handler(event);
+  });
+}
+
+/// Removes an event listener.
+pub fn unlisten(event_id: EventId) {
+  crate::async_runtime::spawn(async move {
+    let mut event_listeners = listeners()
+      .lock()
+      .expect("Failed to lock listeners: listen()");
+    for listeners in event_listeners.values_mut() {
+      if let Some(index) = listeners.iter().position(|l| l.id == event_id) {
+        listeners.remove(index);
+      }
+    }
+  })
 }
 
 /// Emits an event to JS.
@@ -82,7 +134,7 @@ pub fn emit<D: ApplicationDispatcherExt, S: Serialize>(
   };
 
   webview_dispatcher.eval(&format!(
-    "window['{}']({{type: '{}', payload: {}}}, '{}')",
+    "window['{}']({{event: '{}', payload: {}}}, '{}')",
     emit_function_name(),
     event.as_ref(),
     js_payload,
@@ -93,7 +145,7 @@ pub fn emit<D: ApplicationDispatcherExt, S: Serialize>(
 }
 
 /// Triggers the given event with its payload.
-pub fn on_event(event: String, window_label: Option<&str>, data: Option<String>) {
+pub(crate) fn on_event(event: String, window_label: Option<&str>, data: Option<String>) {
   let mut l = listeners()
     .lock()
     .expect("Failed to lock listeners: on_event()");
@@ -104,11 +156,19 @@ pub fn on_event(event: String, window_label: Option<&str>, data: Option<String>)
       if let Some(target_window_label) = window_label {
         // if the emitted event targets a specifid window, only triggers the listeners associated to that window
         if handler.window_label.as_deref() == Some(target_window_label) {
-          (handler.on_event)(data.clone())
+          let payload = data.clone();
+          (handler.on_event)(EventPayload {
+            id: handler.id,
+            payload,
+          });
         }
       } else {
         // otherwise triggers all listeners
-        (handler.on_event)(data.clone())
+        let payload = data.clone();
+        (handler.on_event)(EventPayload {
+          id: handler.id,
+          payload,
+        });
       }
     }
   }
@@ -120,7 +180,7 @@ mod test {
   use proptest::prelude::*;
 
   // dummy event handler function
-  fn event_fn(s: Option<String>) {
+  fn event_fn(s: EventPayload) {
     println!("{:?}", s);
   }
 

+ 5 - 6
tauri/src/app/utils.rs

@@ -98,11 +98,11 @@ fn event_initialization_script() -> String {
   return format!(
     "
       window['{queue}'] = [];
-      window['{fn}'] = function (payload, salt, ignoreQueue) {{
-      const listeners = (window['{listeners}'] && window['{listeners}'][payload.type]) || []
+      window['{fn}'] = function (eventData, salt, ignoreQueue) {{
+      const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
       if (!ignoreQueue && listeners.length === 0) {{
         window['{queue}'].push({{
-          payload: payload,
+          eventData: eventData,
           salt: salt
         }})
       }}
@@ -118,9 +118,8 @@ fn event_initialization_script() -> String {
           if (flag) {{
             for (let i = listeners.length - 1; i >= 0; i--) {{
               const listener = listeners[i]
-              if (listener.once)
-                listeners.splice(i, 1)
-              listener.handler(payload)
+              eventData.id = listener.id
+              listener.handler(eventData)
             }}
           }}
         }})

+ 25 - 4
tauri/src/app/webview_manager.rs

@@ -4,6 +4,7 @@ use std::{
 };
 
 use super::{
+  event::{EventId, EventPayload},
   App, ApplicationDispatcherExt, ApplicationExt, Icon, Webview, WebviewBuilderExt,
   WebviewInitializer,
 };
@@ -32,14 +33,24 @@ impl<A: ApplicationDispatcherExt> WebviewDispatcher<A> {
   }
 
   /// Listen to a webview event.
-  pub fn listen<F: FnMut(Option<String>) + Send + 'static>(
+  pub fn listen<F: Fn(EventPayload) + Send + 'static>(
     &self,
     event: impl AsRef<str>,
     handler: F,
-  ) {
+  ) -> EventId {
     super::event::listen(event, Some(self.window_label.to_string()), handler)
   }
 
+  /// Listen to a webview event and unlisten after the first event.
+  pub fn once<F: Fn(EventPayload) + Send + 'static>(&self, event: impl AsRef<str>, handler: F) {
+    super::event::once(event, Some(self.window_label.to_string()), handler)
+  }
+
+  /// Unregister the event listener with the given id.
+  pub fn unlisten(&self, event_id: EventId) {
+    super::event::unlisten(event_id)
+  }
+
   /// Emits an event to the webview.
   pub fn emit<S: Serialize>(
     &self,
@@ -277,14 +288,24 @@ impl<A: ApplicationExt + 'static> WebviewManager<A> {
 
   /// Listen to a global event.
   /// An event from any webview will trigger the handler.
-  pub fn listen<F: FnMut(Option<String>) + Send + 'static>(
+  pub fn listen<F: Fn(EventPayload) + Send + 'static>(
     &self,
     event: impl AsRef<str>,
     handler: F,
-  ) {
+  ) -> EventId {
     super::event::listen(event, None, handler)
   }
 
+  /// Listen to a global event and unlisten after the first event.
+  pub fn once<F: Fn(EventPayload) + Send + 'static>(&self, event: impl AsRef<str>, handler: F) {
+    super::event::once(event, None, handler)
+  }
+
+  /// Unregister the global event listener with the given id.
+  pub fn unlisten(&self, event_id: EventId) {
+    super::event::unlisten(event_id)
+  }
+
   /// Emits an event to all webviews.
   pub fn emit<S: Serialize + Clone>(
     &self,

+ 6 - 6
tauri/src/async_runtime.rs

@@ -2,13 +2,13 @@ use once_cell::sync::OnceCell;
 use tokio::runtime::Runtime;
 pub use tokio::sync::Mutex;
 
-use std::{future::Future, sync::Mutex as StdMutex};
+use std::future::Future;
 
-static RUNTIME: OnceCell<StdMutex<Runtime>> = OnceCell::new();
+static RUNTIME: OnceCell<Runtime> = OnceCell::new();
 
 pub fn block_on<F: Future>(task: F) -> F::Output {
-  let runtime = RUNTIME.get_or_init(|| StdMutex::new(Runtime::new().unwrap()));
-  runtime.lock().unwrap().block_on(task)
+  let runtime = RUNTIME.get_or_init(|| Runtime::new().unwrap());
+  runtime.block_on(task)
 }
 
 pub fn spawn<F>(task: F)
@@ -16,6 +16,6 @@ where
   F: Future + Send + 'static,
   F::Output: Send + 'static,
 {
-  let runtime = RUNTIME.get_or_init(|| StdMutex::new(Runtime::new().unwrap()));
-  runtime.lock().unwrap().spawn(task);
+  let runtime = RUNTIME.get_or_init(|| Runtime::new().unwrap());
+  runtime.spawn(task);
 }

+ 48 - 30
tauri/src/endpoints/event.rs

@@ -6,12 +6,10 @@ use serde::Deserialize;
 #[serde(tag = "cmd", rename_all = "camelCase")]
 pub enum Cmd {
   /// Listen to an event.
-  Listen {
-    event: String,
-    handler: String,
-    #[serde(default)]
-    once: bool,
-  },
+  Listen { event: String, handler: String },
+  /// Unlisten to an event.
+  #[serde(rename_all = "camelCase")]
+  Unlisten { event_id: u64 },
   /// Emit an event to the webview associated with the given window.
   /// If the window_label is omitted, the event will be triggered on all listeners.
   #[serde(rename_all = "camelCase")]
@@ -28,13 +26,18 @@ impl Cmd {
     webview_manager: &crate::WebviewManager<A>,
   ) -> crate::Result<InvokeResponse> {
     match self {
-      Self::Listen {
-        event,
-        handler,
-        once,
-      } => {
-        let js_string = listen_fn(event, handler, once)?;
-        webview_manager.current_webview()?.eval(&js_string)?;
+      Self::Listen { event, handler } => {
+        let event_id = rand::random();
+        webview_manager
+          .current_webview()?
+          .eval(&listen_js(event, event_id, handler))?;
+        Ok(event_id.into())
+      }
+      Self::Unlisten { event_id } => {
+        webview_manager
+          .current_webview()?
+          .eval(&unlisten_js(event_id))?;
+        Ok(().into())
       }
       Self::Emit {
         event,
@@ -53,48 +56,63 @@ impl Cmd {
           // dispatch the event to JS listeners
           webview_manager.emit(event, payload)?;
         }
+        Ok(().into())
       }
     }
-    Ok(().into())
   }
 }
 
-pub fn listen_fn(event: String, handler: String, once: bool) -> crate::Result<String> {
-  Ok(format!(
+pub fn unlisten_js(event_id: u64) -> String {
+  format!(
+    "
+      for (var event in (window['{listeners}'] || {{}})) {{
+        var listeners = (window['{listeners}'] || {{}})[event]
+        if (listeners) {{
+          window['{listeners}'][event] = window['{listeners}'][event].filter(function (e) {{ e.id !== {event_id} }})
+        }}
+      }}
+    ",
+    listeners = crate::app::event::event_listeners_object_name(),
+    event_id = event_id,
+  )
+}
+
+pub fn listen_js(event: String, event_id: u64, handler: String) -> String {
+  format!(
     "if (window['{listeners}'] === void 0) {{
       window['{listeners}'] = {{}}
-      }}
-    if (window['{listeners}']['{evt}'] === void 0) {{
-      window['{listeners}']['{evt}'] = []
     }}
-    window['{listeners}']['{evt}'].push({{
-      handler: window['{handler}'],
-      once: {once_flag}
+    if (window['{listeners}']['{event}'] === void 0) {{
+      window['{listeners}']['{event}'] = []
+    }}
+    window['{listeners}']['{event}'].push({{
+      id: {event_id},
+      handler: window['{handler}']
     }});
 
     for (let i = 0; i < (window['{queue}'] || []).length; i++) {{
       const e = window['{queue}'][i];
-      window['{emit}'](e.payload, e.salt, true)
+      window['{emit}'](e.eventData, e.salt, true)
     }}
   ",
     listeners = crate::app::event::event_listeners_object_name(),
     queue = crate::app::event::event_queue_object_name(),
     emit = crate::app::event::emit_function_name(),
-    evt = event,
-    handler = handler,
-    once_flag = if once { "true" } else { "false" }
-  ))
+    event = event,
+    event_id = event_id,
+    handler = handler
+  )
 }
 
 #[cfg(test)]
 mod test {
   use proptest::prelude::*;
 
-  // check the listen_fn for various usecases.
+  // check the listen_js for various usecases.
   proptest! {
     #[test]
-    fn check_listen_fn(event in "", handler in "", once in proptest::bool::ANY) {
-      super::listen_fn(event, handler, once).expect("listen_fn failed");
+    fn check_listen_js(event in "", id in proptest::bits::u64::ANY, handler in "") {
+      super::listen_js(event, id, handler);
     }
   }
 }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików