Tauri のイベントハンドリングについて - Tauri Events -

# はじめに

例えば、GUI ツールのログを出していている際、その処理自体はバックエンドの Rust 側で起きていることだが、これをリアルタイムかつ、 TypeScript + フロントエンド上で扱い、表示をしたいと言ったこともあると思う。
こうした、サービス型のプロセスは、ユーザー自ら実行するタイプの Tauri コマンドではできない。

そこで、Tauri には [Events](https://tauri.app/v1/guides/features/events) という機能があるという事を見つけた。
これを使う事で解決が可能ではないかと思い立ったので、これについての実装を検証してみることにする

### 環境

* Windows 11 Pro 23H2
* Tauri 1.5.3
* Rust 1.74.1
* Svelte 4.2.7

### サンプルコード

https://github.com/takavfx/learn_tauri_examples/tree/2024.3.8_1/events

# Tauri Events

Events はドキュメントによると、**フロントエンドとバックエンド間**で**マルチプロデューサー・マルチコンシューマー コミュニケーション**を可能にする機能とのこと。

https://tauri.app/v1/guides/features/events

Tauri Events には二種類あり、「グローバルイベント (Global Event) 」と「ウィンドウイベント (Window Event)」がある。

それぞれに使い方の差異はあるようだが、設定の方法はあまり変わらないためここでは「グローバルイベント」の設定方法をベースに記述する。

:::note info
**マルチプロデューサー/マルチコンシューマーとは**
そもそも、プロデューサー(生産者)/コンシューマー(消費者)という構造は[マルチスレッド](https://docs.xilinx.com/r/ja-JP/ug1399-vitis-hls/%E3%83%97%E3%83%AD%E3%83%87%E3%83%A5%E3%83%BC%E3%82%B5%E3%83%BC/%E3%82%B3%E3%83%B3%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%9E%E3%83%BC-%E3%83%91%E3%83%A9%E3%83%80%E3%82%A4%E3%83%A0)やネットワーク通信でよく用いられる用語らしい。

今回のケースは後者の方で、[こちら](https://www.keyence.co.jp/ss/products/controls/network/basics/communication.jsp)によると、
> プロデューサはデータを作成して次々に送信していきます。
> コンシューマ側では受信したデータの中に格納されたIDを確認し、自分に関連するID情報を順番に処理していきます。

とあります。
Tauri Events の場合は、これをより複数送信者・受信者を持った状態で通信できる機構といったことの様です。
:::

## フロントエンド -> バックエンド

```mermaid
sequenceDiagram
    participant Frontend
    participant Backend
    Frontend->>Backend: Event: Click +  Event Message
```

```svelte:+page.svelte
<script lang="ts">
	import { emit, listen } from '@tauri-apps/api/event';

	async function onClicked() {
		emit('click', { theMessage: 'Tauri is awsome!' });
	}
</script>

<button on:click={onClicked}>Hello</button>

```

```rust:main.rs
use tauri::Manager;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let id = app.listen_global("click", |event| {
                println!("got `click` event with payload {:?}", event.payload());
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

以下のように Hello ボタンをクリックすると、バックエンド側の出力にメッセージがプリントされる。

![output-palette.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/5b284dd7-f0b0-3006-008d-a1b9836e8160.gif)


:::note info
**特定の条件下でリッスン状態を止めたい**
例えば、環境変数に「TAURI_UNLISTEN=1」が設定されていればリッスン状態を解除する。

```diff_rust:main.rs
fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let id = app.listen_global("click", |event| {
                println!("got `click` event with payload {:?}",
                    event.payload());
            });

+           match env::var("TAURI_UNLISTEN") {
+               Ok(val) => {
+                   let value: u8 = val.parse().unwrap();
+                   if value == 1 {
+                       app.unlisten(id);
+                   }
+               }
+               _ => (),
+           }

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```
:::

## フロントエンド <- バックエンド

```mermaid
sequenceDiagram
    participant Frontend
    participant Backend
    Backend->>Frontend: Event: Count + Event Message
```

こちらは、バックエンド (Rust) 側での操作をフロントエンド側へ反映したい場合に用いる。
こちらの用法が Tarui Events としての**利用頻度がかなり高くなる**手法。

例えば、以下の様にバックエンド側でイベントループを発生させている際に、定時実行でフロントエンド側に反映させたいとるする。この場合は、フロントエンド側からの Tarui Command の実行は期待できないので、バックエンド側から能動的に反映処理を行う必要がある。この場合には Tauri Events を使う事になる。

```rust:main.rs
use chrono::Utc;
use cron::Schedule;
use serde::Serialize;
use std::{env, str::FromStr, thread::sleep, time::Duration};
use tauri::Manager;

#[derive(Clone, Serialize)]
struct CountEventPayload {
    count: u32,
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            // Click event ==========
            // ... 省略

            // Count Event ==========
            // アプリハンドルを先に作成しておく。
            // つい、app を渡したくなってしまうが、この app は &mut App といった
            // Mutabble な値なため、非同期時の値の整合性の保証がなくなってしまう。
            // それ故、そのまま非同期ランタイムに渡そうとするとコンパイラに怒られる。
            let app_handle = app.app_handle();

            // そのままループを作るとメイン処理が固まるので、tauri::async_runtime
            // で非同期ランタイムを作成
            let _count_handle = tauri::async_runtime::spawn(async move {
                // cron 式で3秒ごとのイベントをスケジュールする。
                let schedule = Schedule::from_str("0/3 * * * * *").unwrap();
                let mut count: u32 = 0;
                let mut next_tick = schedule.upcoming(Utc).next().unwrap();
                
                loop {
                    let now = Utc::now();

                    // 条件に入った際の処理
                    if now >= next_tick {
                        next_tick = schedule.upcoming(Utc).next().unwrap();
                        // 1 カウントする
                        count += 1;
                        
                        // イベントを emit。
                        let result =
                            app_handle.emit_all(
                                "count_event", CountEventPayload { count: count }
                            );
                        match result {
                            Err(ref err) => println!("{:?}", err),
                            _ => (),
                        }
                    }

                    sleep(Duration::from_secs(std::cmp::min(
                        (next_tick - now).num_seconds() as u64,
                        60,
                    )));
                }
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

```svelte:CountEvent.svelte
<script lang="ts">
	import { Button, Indicator } from 'flowbite-svelte';
	import { listen } from '@tauri-apps/api/event';
	import { EnvelopeSolid } from 'flowbite-svelte-icons';

	type Payload = {
		count: number;
	};
 
	let count = 0;

	$: listen('count_event', (event) => {
		let payload = event.payload as Payload;
		count = payload.count;
	});
</script>

<Button class="relative" size="sm">
	<EnvelopeSolid class="text-white dark:text-white" />
	<span class="sr-only">Notifications</span>
	<Indicator color="blue" border size="xl"
     placement="top-right" class="text-xs font-bold"
    >
		{count}
	</Indicator>
</Button>
```

![output-palette2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/d62b9648-ade7-f043-150c-c1adc648fac0.gif)

ちなみに、処理自体は全部バックエンドで管理していあるので、UI 上で再処理がかかって初期値が当てられても、バックエンド上の値が定時で返ってくる。
（実際はタブの切り替えに Tauri Command を仕込んで取得すると言ったことをするだろう。）

![output-palette4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/b46d1696-07f9-d420-1983-02df1b43aeca.gif)


# まとめ

Tauri Events は Tauri Command と同じく、頻繁に使用していく機能だとわかった。

特に Events で重要だと感じたのは、バックエンド -> フロントエンドへの更新のしやすさ。基本的に、バックエンド -> フロントエンドでフロントエンド更新を行いたい場合は Tauri Events を使うことになるだろう。

実際にもう少し作り込んでいけば、かなりトリッキーなこともここら辺を使ってできそうなので、記録しておく。