fix(core): immediately unregister event listener on unlisten call (#13306)

* fix(core): immediately unregister event listener on unlisten call

the unlisten function is currently async, but marked as `() => void` in the TypeScript definition. To avoid a breaking change, we're going to immediately unregister the listener function so it's not called.

this fixes a race condition where after calling unlisten() you would still receive events if you do not `await` it and there's a new event triggering while the unlisten command is running

* cleanup

* fix build

* fix ci
This commit is contained in:
Lucas Fernandes Nogueira
2025-05-05 10:46:05 -03:00
committed by GitHub
parent c84b162374
commit b985eaf0a2
8 changed files with 62 additions and 20 deletions

View File

@@ -0,0 +1,6 @@
---
"@tauri-apps/api": minor:bug
"tauri": minor:bug
---
Immediately unregister event listener when the unlisten function is called.

File diff suppressed because one or more lines are too long

View File

@@ -1102,7 +1102,7 @@ impl<R: Runtime> App<R> {
)]
fn register_core_plugins(&self) -> crate::Result<()> {
self.handle.plugin(crate::path::plugin::init())?;
self.handle.plugin(crate::event::plugin::init())?;
self.handle.plugin(crate::event::plugin::init(self))?;
self.handle.plugin(crate::window::plugin::init())?;
self.handle.plugin(crate::webview::plugin::init())?;
self.handle.plugin(crate::app::plugin::init())?;

View File

@@ -0,0 +1,10 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// eslint-disable-next-line
Object.defineProperty(window, '__TAURI_EVENT_PLUGIN_INTERNALS__', {
value: {
unregisterListener: __RAW_unregister_listener_function__
}
})

View File

@@ -164,7 +164,7 @@ impl Event {
}
}
pub fn listen_js_script(
pub(crate) fn listen_js_script(
listeners_object_name: &str,
serialized_target: &str,
event: EventName<&str>,
@@ -191,7 +191,7 @@ pub fn listen_js_script(
)
}
pub fn emit_js_script(
pub(crate) fn emit_js_script(
event_emit_function_name: &str,
emit_args: &EmitArgs,
serialized_ids: &str,
@@ -205,23 +205,23 @@ pub fn emit_js_script(
))
}
pub fn unlisten_js_script(
pub(crate) fn unlisten_js_script(
listeners_object_name: &str,
event_name: EventName<&str>,
event_id: EventId,
event_arg: &str,
event_id_arg: &str,
) -> String {
format!(
"(function () {{
const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}']
const listeners = (window['{listeners_object_name}'] || {{}})[{event_arg}]
if (listeners) {{
window.__TAURI_INTERNALS__.unregisterCallback(listeners[{event_id}].handlerId)
window.__TAURI_INTERNALS__.unregisterCallback(listeners[{event_id_arg}].handlerId)
}}
}})()
",
)
}
pub fn event_initialization_script(function_name: &str, listeners: &str) -> String {
pub(crate) fn event_initialization_script(function_name: &str, listeners: &str) -> String {
format!(
"Object.defineProperty(window, '{function_name}', {{
value: function (eventData, ids) {{

View File

@@ -3,11 +3,12 @@
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Deserializer};
use serde_json::Value as JsonValue;
use serialize_to_javascript::{default_template, DefaultTemplate, Template};
use tauri_runtime::window::is_label_valid;
use crate::plugin::{Builder, TauriPlugin};
use crate::{command, ipc::CallbackFn, EventId, Result, Runtime};
use crate::{AppHandle, Emitter, Webview};
use crate::{AppHandle, Emitter, Manager, Webview};
use super::EventName;
use super::EventTarget;
@@ -75,11 +76,33 @@ async fn emit_to<R: Runtime>(
}
/// Initializes the event plugin.
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
pub(crate) fn init<R: Runtime, M: Manager<R>>(manager: &M) -> TauriPlugin<R> {
let listeners = manager.manager().listeners();
#[derive(Template)]
#[default_template("./init.js")]
struct InitJavascript {
#[raw]
unregister_listener_function: String,
}
let init_script = InitJavascript {
unregister_listener_function: format!(
"(event, eventId) => {}",
crate::event::unlisten_js_script(listeners.listeners_object_name(), "event", "eventId")
),
};
Builder::new("event")
.invoke_handler(crate::generate_handler![
#![plugin(event)]
listen, unlisten, emit, emit_to
])
.js_init_script(
init_script
.render_default(&Default::default())
.unwrap()
.to_string(),
)
.build()
}

View File

@@ -1680,12 +1680,6 @@ fn main() {
pub(crate) fn unlisten_js(&self, event: EventName<&str>, id: EventId) -> crate::Result<()> {
let listeners = self.manager().listeners();
self.eval(crate::event::unlisten_js_script(
listeners.listeners_object_name(),
event,
id,
))?;
listeners.unlisten_js(event, id);
Ok(())

View File

@@ -11,6 +11,14 @@
import { invoke, transformCallback } from './core'
declare global {
interface Window {
__TAURI_EVENT_PLUGIN_INTERNALS__: {
unregisterListener: (event: string, eventId: number) => void
}
}
}
type EventTarget =
| { kind: 'Any' }
| { kind: 'AnyLabel'; label: string }
@@ -30,6 +38,7 @@ interface Event<T> {
type EventCallback<T> = (event: Event<T>) => void
// TODO(v3): mark this as Promise<void>
type UnlistenFn = () => void
type EventName = `${TauriEvent}` | (string & Record<never, never>)
@@ -72,6 +81,7 @@ enum TauriEvent {
* @returns
*/
async function _unlisten(event: string, eventId: number): Promise<void> {
window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(event, eventId)
await invoke('plugin:event|unlisten', {
event,
eventId
@@ -152,8 +162,7 @@ async function once<T>(
return listen<T>(
event,
(eventData) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
_unlisten(event, eventData.id)
void _unlisten(event, eventData.id)
handler(eventData)
},
options