Skip to content

Commit

Permalink
refactor(core): add unlisten, once APIs to the event system (#1359)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Mar 16, 2021
1 parent 46f3d5f commit b670ec5
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 106 deletions.
6 changes: 6 additions & 0 deletions .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 changes: 5 additions & 0 deletions .changes/event-unlisten-rust.md
@@ -0,0 +1,5 @@
---
"tauri": minor
---

Adds `unlisten` and `once` APIs on the Rust event system.
47 changes: 36 additions & 11 deletions api/src/helpers/event.ts
Expand Up @@ -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
}
})
}
Expand All @@ -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)
}

/**
Expand All @@ -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(() => {})
})
}

/**
Expand Down
12 changes: 8 additions & 4 deletions 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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
}
Expand Down
4 changes: 2 additions & 2 deletions examples/api/public/build/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/api/public/build/bundle.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions examples/api/src-tauri/src/main.rs
Expand Up @@ -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(),
};
Expand Down
38 changes: 11 additions & 27 deletions examples/api/src/App.svelte
Expand Up @@ -61,6 +61,7 @@
function onMessage(value) {
responses += typeof value === "string" ? value : JSON.stringify(value);
responses += "\n";
}
function onLogoClick() {
Expand All @@ -72,38 +73,24 @@
<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>
</div>
<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">
Expand All @@ -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>
2 changes: 1 addition & 1 deletion examples/api/src/components/Communication.svelte
Expand Up @@ -4,7 +4,7 @@
export let onMessage;
listen("rust-event", onMessage);
listen("rust-event", onMessage)
function log() {
invoke("log_operation", {
Expand Down
2 changes: 1 addition & 1 deletion examples/multiwindow/dist/__tauri.js

Large diffs are not rendered by default.

Binary file removed tauri/examples/api/public/tauri.png
Binary file not shown.
82 changes: 71 additions & 11 deletions tauri/src/app/event.rs
Expand Up @@ -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>>>>;
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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()");
Expand All @@ -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,
});
}
}
}
Expand All @@ -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);
}

Expand Down
11 changes: 5 additions & 6 deletions tauri/src/app/utils.rs
Expand Up @@ -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
}})
}}
Expand All @@ -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)
}}
}}
}})
Expand Down

0 comments on commit b670ec5

Please sign in to comment.