Skip to content

Commit b670ec5

Browse files
authored
refactor(core): add unlisten, once APIs to the event system (#1359)
1 parent 46f3d5f commit b670ec5

File tree

16 files changed

+228
-106
lines changed

16 files changed

+228
-106
lines changed

.changes/event-unlisten-js.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri-api": minor
3+
"tauri": minor
4+
---
5+
6+
Refactor the event callback payload and return an unlisten function on the `listen` API.

.changes/event-unlisten-rust.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": minor
3+
---
4+
5+
Adds `unlisten` and `once` APIs on the Rust event system.

api/src/helpers/event.ts

+36-11
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,45 @@ import { invokeTauriCommand } from './tauri'
22
import { transformCallback } from '../tauri'
33

44
export interface Event<T> {
5-
type: string
5+
/// event name.
6+
event: string
7+
/// event identifier used to unlisten.
8+
id: number
9+
/// event payload.
610
payload: T
711
}
812

913
export type EventCallback<T> = (event: Event<T>) => void
1014

15+
export type UnlistenFn = () => void
16+
1117
async function _listen<T>(
1218
event: string,
13-
handler: EventCallback<T>,
14-
once: boolean
15-
): Promise<void> {
16-
await invokeTauriCommand({
19+
handler: EventCallback<T>
20+
): Promise<UnlistenFn> {
21+
return invokeTauriCommand<number>({
1722
__tauriModule: 'Event',
1823
message: {
1924
cmd: 'listen',
2025
event,
21-
handler: transformCallback(handler, once),
22-
once
26+
handler: transformCallback(handler)
27+
}
28+
}).then((eventId) => {
29+
return async () => _unlisten(eventId)
30+
})
31+
}
32+
33+
/**
34+
* Unregister the event listener associated with the given id.
35+
*
36+
* @param {number} eventId the event identifier
37+
*/
38+
async function _unlisten(eventId: number): Promise<void> {
39+
return invokeTauriCommand({
40+
__tauriModule: 'Event',
41+
message: {
42+
cmd: 'unlisten',
43+
eventId
2344
}
2445
})
2546
}
@@ -29,12 +50,13 @@ async function _listen<T>(
2950
*
3051
* @param event the event name
3152
* @param handler the event handler callback
53+
* @return {Promise<UnlistenFn>} a promise resolving to a function to unlisten to the event.
3254
*/
3355
async function listen<T>(
3456
event: string,
3557
handler: EventCallback<T>
36-
): Promise<void> {
37-
return _listen(event, handler, false)
58+
): Promise<UnlistenFn> {
59+
return _listen(event, handler)
3860
}
3961

4062
/**
@@ -46,8 +68,11 @@ async function listen<T>(
4668
async function once<T>(
4769
event: string,
4870
handler: EventCallback<T>
49-
): Promise<void> {
50-
return _listen(event, handler, true)
71+
): Promise<UnlistenFn> {
72+
return _listen<T>(event, (eventData) => {
73+
handler(eventData)
74+
_unlisten(eventData.id).catch(() => {})
75+
})
5176
}
5277

5378
/**

api/src/window.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { invokeTauriCommand } from './helpers/tauri'
2-
import { EventCallback, emit, listen, once } from './helpers/event'
2+
import { EventCallback, UnlistenFn, emit, listen, once } from './helpers/event'
33

44
interface WindowDef {
55
label: string
@@ -39,10 +39,14 @@ class WebviewWindowHandle {
3939
*
4040
* @param event the event name
4141
* @param handler the event handler callback
42+
* @return {Promise<UnlistenFn>} a promise resolving to a function to unlisten to the event.
4243
*/
43-
async listen<T>(event: string, handler: EventCallback<T>): Promise<void> {
44+
async listen<T>(
45+
event: string,
46+
handler: EventCallback<T>
47+
): Promise<UnlistenFn> {
4448
if (this._handleTauriEvent(event, handler)) {
45-
return Promise.resolve()
49+
return Promise.resolve(() => {})
4650
}
4751
return listen(event, handler)
4852
}
@@ -70,7 +74,7 @@ class WebviewWindowHandle {
7074
if (localTauriEvents.includes(event)) {
7175
// eslint-disable-next-line
7276
for (const handler of this.listeners[event] || []) {
73-
handler({ type: event, payload })
77+
handler({ event, id: -1, payload })
7478
}
7579
return Promise.resolve()
7680
}

examples/api/public/build/bundle.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/api/public/build/bundle.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ fn main() {
1717
.setup(|webview_manager| async move {
1818
let dispatcher = webview_manager.current_webview().unwrap();
1919
let dispatcher_ = dispatcher.clone();
20-
dispatcher.listen("js-event", move |msg| {
21-
println!("got js-event with message '{:?}'", msg);
20+
dispatcher.listen("js-event", move |event| {
21+
println!("got js-event with message '{:?}'", event.payload());
2222
let reply = Reply {
2323
data: "something else".to_string(),
2424
};

examples/api/src/App.svelte

+11-27
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
6262
function onMessage(value) {
6363
responses += typeof value === "string" ? value : JSON.stringify(value);
64+
responses += "\n";
6465
}
6566
6667
function onLogoClick() {
@@ -72,38 +73,24 @@
7273
<div class="flex row noselect just-around" style="margin=1em;">
7374
<img src="tauri.png" height="60" on:click={onLogoClick} alt="logo" />
7475
<div>
75-
<a
76-
class="dark-link"
77-
target="_blank"
78-
href="https://tauri.studio/en/docs/getting-started/intro"
79-
>
76+
<a class="dark-link" target="_blank" href="https://tauri.studio/en/docs/getting-started/intro">
8077
Documentation
8178
</a>
82-
<a
83-
class="dark-link"
84-
target="_blank"
85-
href="https://github.com/tauri-apps/tauri"
86-
>
79+
<a class="dark-link" target="_blank" href="https://github.com/tauri-apps/tauri">
8780
Github
8881
</a>
89-
<a
90-
class="dark-link"
91-
target="_blank"
92-
href="https://github.com/tauri-apps/tauri/tree/dev/tauri/examples/api"
93-
>
82+
<a class="dark-link" target="_blank" href="https://github.com/tauri-apps/tauri/tree/dev/tauri/examples/api">
9483
Source
9584
</a>
9685
</div>
9786
</div>
9887
<div class="flex row">
9988
<div style="width:15em; margin-left:0.5em">
10089
{#each views as view}
101-
<p
102-
class="nv noselect {selected === view ? 'nv_selected' : ''}"
103-
on:click={() => select(view)}
90+
<p class="nv noselect {selected === view ? 'nv_selected' : ''}" on:click={()=> select(view)}
10491
>
105-
{view.label}
106-
</p>
92+
{view.label}
93+
</p>
10794
{/each}
10895
</div>
10996
<div class="content">
@@ -113,13 +100,10 @@
113100
<div id="response">
114101
<p class="flex row just-around">
115102
<strong>Tauri Console</strong>
116-
<a
117-
class="nv"
118-
on:click={() => {
119-
responses = [""];
120-
}}>clear</a
121-
>
103+
<a class="nv" on:click={()=> {
104+
responses = [""];
105+
}}>clear</a>
122106
</p>
123107
{responses}
124108
</div>
125-
</main>
109+
</main>

examples/api/src/components/Communication.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
export let onMessage;
66
7-
listen("rust-event", onMessage);
7+
listen("rust-event", onMessage)
88
99
function log() {
1010
invoke("log_operation", {

examples/multiwindow/dist/__tauri.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tauri/examples/api/public/tauri.png

-4.47 KB
Binary file not shown.

tauri/src/app/event.rs

+71-11
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ use once_cell::sync::Lazy;
1010
use serde::Serialize;
1111
use serde_json::Value as JsonValue;
1212

13+
/// Event identifier.
14+
pub type EventId = u64;
15+
1316
/// An event handler.
1417
struct EventHandler {
18+
/// Event identifier.
19+
id: EventId,
1520
/// A event handler might be global or tied to a window.
1621
window_label: Option<String>,
1722
/// The on event callback.
18-
on_event: Box<dyn FnMut(Option<String>) + Send>,
23+
on_event: Box<dyn Fn(EventPayload) + Send>,
1924
}
2025

2126
type Listeners = Arc<Mutex<HashMap<String, Vec<EventHandler>>>>;
@@ -47,24 +52,71 @@ pub fn event_queue_object_name() -> String {
4752
EVENT_QUEUE_OBJECT_NAME.to_string()
4853
}
4954

55+
#[derive(Debug, Clone)]
56+
pub struct EventPayload {
57+
id: EventId,
58+
payload: Option<String>,
59+
}
60+
61+
impl EventPayload {
62+
/// The event identifier.
63+
pub fn id(&self) -> EventId {
64+
self.id
65+
}
66+
67+
/// The event payload.
68+
pub fn payload(&self) -> Option<&String> {
69+
self.payload.as_ref()
70+
}
71+
}
72+
5073
/// Adds an event listener for JS events.
51-
pub fn listen<F: FnMut(Option<String>) + Send + 'static>(
52-
id: impl AsRef<str>,
74+
pub fn listen<F: Fn(EventPayload) + Send + 'static>(
75+
event_name: impl AsRef<str>,
5376
window_label: Option<String>,
5477
handler: F,
55-
) {
78+
) -> EventId {
5679
let mut l = listeners()
5780
.lock()
5881
.expect("Failed to lock listeners: listen()");
82+
let id = rand::random();
5983
let handler = EventHandler {
84+
id,
6085
window_label,
6186
on_event: Box::new(handler),
6287
};
63-
if let Some(listeners) = l.get_mut(id.as_ref()) {
88+
if let Some(listeners) = l.get_mut(event_name.as_ref()) {
6489
listeners.push(handler);
6590
} else {
66-
l.insert(id.as_ref().to_string(), vec![handler]);
91+
l.insert(event_name.as_ref().to_string(), vec![handler]);
6792
}
93+
id
94+
}
95+
96+
/// Listen to an JS event and immediately unlisten.
97+
pub fn once<F: Fn(EventPayload) + Send + 'static>(
98+
event_name: impl AsRef<str>,
99+
window_label: Option<String>,
100+
handler: F,
101+
) {
102+
listen(event_name, window_label, move |event| {
103+
unlisten(event.id);
104+
handler(event);
105+
});
106+
}
107+
108+
/// Removes an event listener.
109+
pub fn unlisten(event_id: EventId) {
110+
crate::async_runtime::spawn(async move {
111+
let mut event_listeners = listeners()
112+
.lock()
113+
.expect("Failed to lock listeners: listen()");
114+
for listeners in event_listeners.values_mut() {
115+
if let Some(index) = listeners.iter().position(|l| l.id == event_id) {
116+
listeners.remove(index);
117+
}
118+
}
119+
})
68120
}
69121

70122
/// Emits an event to JS.
@@ -82,7 +134,7 @@ pub fn emit<D: ApplicationDispatcherExt, S: Serialize>(
82134
};
83135

84136
webview_dispatcher.eval(&format!(
85-
"window['{}']({{type: '{}', payload: {}}}, '{}')",
137+
"window['{}']({{event: '{}', payload: {}}}, '{}')",
86138
emit_function_name(),
87139
event.as_ref(),
88140
js_payload,
@@ -93,7 +145,7 @@ pub fn emit<D: ApplicationDispatcherExt, S: Serialize>(
93145
}
94146

95147
/// Triggers the given event with its payload.
96-
pub fn on_event(event: String, window_label: Option<&str>, data: Option<String>) {
148+
pub(crate) fn on_event(event: String, window_label: Option<&str>, data: Option<String>) {
97149
let mut l = listeners()
98150
.lock()
99151
.expect("Failed to lock listeners: on_event()");
@@ -104,11 +156,19 @@ pub fn on_event(event: String, window_label: Option<&str>, data: Option<String>)
104156
if let Some(target_window_label) = window_label {
105157
// if the emitted event targets a specifid window, only triggers the listeners associated to that window
106158
if handler.window_label.as_deref() == Some(target_window_label) {
107-
(handler.on_event)(data.clone())
159+
let payload = data.clone();
160+
(handler.on_event)(EventPayload {
161+
id: handler.id,
162+
payload,
163+
});
108164
}
109165
} else {
110166
// otherwise triggers all listeners
111-
(handler.on_event)(data.clone())
167+
let payload = data.clone();
168+
(handler.on_event)(EventPayload {
169+
id: handler.id,
170+
payload,
171+
});
112172
}
113173
}
114174
}
@@ -120,7 +180,7 @@ mod test {
120180
use proptest::prelude::*;
121181

122182
// dummy event handler function
123-
fn event_fn(s: Option<String>) {
183+
fn event_fn(s: EventPayload) {
124184
println!("{:?}", s);
125185
}
126186

tauri/src/app/utils.rs

+5-6
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ fn event_initialization_script() -> String {
9898
return format!(
9999
"
100100
window['{queue}'] = [];
101-
window['{fn}'] = function (payload, salt, ignoreQueue) {{
102-
const listeners = (window['{listeners}'] && window['{listeners}'][payload.type]) || []
101+
window['{fn}'] = function (eventData, salt, ignoreQueue) {{
102+
const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
103103
if (!ignoreQueue && listeners.length === 0) {{
104104
window['{queue}'].push({{
105-
payload: payload,
105+
eventData: eventData,
106106
salt: salt
107107
}})
108108
}}
@@ -118,9 +118,8 @@ fn event_initialization_script() -> String {
118118
if (flag) {{
119119
for (let i = listeners.length - 1; i >= 0; i--) {{
120120
const listener = listeners[i]
121-
if (listener.once)
122-
listeners.splice(i, 1)
123-
listener.handler(payload)
121+
eventData.id = listener.id
122+
listener.handler(eventData)
124123
}}
125124
}}
126125
}})

0 commit comments

Comments
 (0)