Tauriの基礎 - 非同期Commands

提供: MochiuWiki : SUSE, EC, PCB

概要

Tauriの非同期Commandsは、フロントエンドのUIをブロックせずに、時間のかかる処理をバックエンドで実行するための機能である。

Rustのasync/await構文とtokioランタイムを活用することで、ファイル操作、ネットワーク通信、データベースアクセス等の重い処理を効率的に実行できる。

非同期Commandsを使用しない場合、長時間の処理がUIスレッドをブロックし、アプリケーションがフリーズしたように見える問題が発生する。

Tauri v2では、Channel APIを使用したストリーミング通信もサポートされており、処理の進捗をリアルタイムでフロントエンドに通知できる。

非同期Commandsの主なメリットは以下の通りである。

  • UIの応答性維持
    メインスレッドをブロックせず、バックグラウンドで処理を実行する。
  • 並行処理のサポート
    複数の非同期タスクを同時に実行できる。
  • プログレス通知
    Channel APIを使用して、処理の進捗をリアルタイムで表示できる。
  • キャンセル対応
    長時間処理の途中でキャンセルを受け付けられる。



前提条件

非同期Commandsを使用するには、以下に示す前提条件を満たしている必要がある。

必要な依存関係

Cargo.tomlファイルに以下に示す依存関係を追加する。

 [dependencies]
 tauri = { version = "2", features = ["unstable"] }
 serde = { version = "1", features = ["derive"] }
 tokio = { version = "1", features = ["full"] }
 futures = "0.3"


tokioランタイムの理解

tokioは、Rustの非同期ランタイムであり、非同期タスクのスケジューリングと実行を管理する。

Tauriは内部でtokioを使用しており、非同期Commandsは自動的にtokioランタイム上で実行される。

tokioの主な機能
機能 説明
非同期タスク asyncブロックを並行実行
タイマー sleep、interval等の時間制御
ネットワーク TCP/UDPの非同期I/O
ファイルI/O 非同期ファイル操作
チャネル mpsc、oneshot等の通信手段



async command定義

非同期Commandsは、Rustの async fn 構文を使用して定義する。

基本的な非同期Command

 use tauri::command;
 
 #[command]
 async fn fetch_data(url: String) -> Result<String, String> {
    // 非同期HTTPリクエスト
    let response = reqwest::get(&url)
       .await
       .map_err(|e| format!("Request failed: {}", e))?;
 
    let body = response.text()
       .await
       .map_err(|e| format!("Failed to read response: {}", e))?;
 
    Ok(body)
 }


Tauriでの自動的な非同期処理

Tauriは、async fn で定義されたCommandを自動的に非同期で実行する。

フロントエンド側では、通常の invoke 関数を使用して呼び出すことができる。

 import { invoke } from '@tauri-apps/api/core'
 
 const fetchData = async () => {
   try {
     const result = await invoke<string>('fetch_data', {
       url: 'https://api.example.com/data'
     })
     console.log('Result:', result)
   }
   catch (error) {
     console.error('Error:', error)
   }
 }


並行実行の仕組み

複数の非同期Commandを並行して実行できる。

 import { invoke } from '@tauri-apps/api/core'
 
 interface UserInfo {
   id: number
   name: string
 }
 
 interface UserPosts {
   posts: Array<{ id: number; title: string }>
 }
 
 const fetchUserData = async (userId: number) => {
   // 並行実行
   const [userInfo, userPosts] = await Promise.all([
     invoke<UserInfo>('get_user_info', { userId }),
     invoke<UserPosts>('get_user_posts', { userId })
   ])
 
   return { userInfo, userPosts }
 }


 // Rust側での並行処理
 
 use tokio::join;
 
 #[command]
 async fn fetch_all_data(user_id: u32) -> Result<AllData, String> {
    let (info, posts) = join!(
       fetch_user_info(user_id),
       fetch_user_posts(user_id)
    );
 
    Ok(AllData {
       info: info.map_err(|e| e.to_string())?,
       posts: posts.map_err(|e| e.to_string())?,
    })
 }



tokioランタイム

Tauriは内部でtokioランタイムを使用しており、非同期タスクを効率的に管理する。

Tauriのtokio統合

Tauriアプリケーションは、起動時にtokioランタイムを初期化する。

 fn main() {
    tauri::Builder::default()
       .invoke_handler(tauri::generate_handler![
          fetch_data,
          fetch_all_data,
          process_file,
       ])
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }


非同期タスクの実行

tokio::spawn を使用して、バックグラウンドでタスクを実行できる。

 use tokio::spawn;
 use std::sync::Arc;
 use tokio::sync::Mutex;
 
 #[command]
 async fn start_background_task() -> Result<String, String> {
    let handle = spawn(async move {
       // バックグラウンドで実行される処理
       for i in 0..10 {
          tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
          println!("Background task: {}", i);
       }
       "Task completed"
    });
 
    // タスクの完了を待たずに戻る
    Ok("Task started in background".to_string())
 }


並行処理のパターン

複数の非同期タスクを管理するパターンを以下に示す。

 use tokio::task::JoinSet;
 
 #[command]
 async fn process_multiple_files(paths: Vec<String>) -> Result<Vec<FileResult>, String> {
    let mut set = JoinSet::new();
 
    for path in paths {
       set.spawn(async move {
          process_single_file(path).await
       });
    }
 
    let mut results = Vec::new();
    while let Some(result) = set.join_next().await {
       match result {
          Ok(Ok(file_result)) => results.push(file_result),
          Ok(Err(e)) => return Err(e),
          Err(e) => return Err(format!("Task failed: {}", e)),
       }
    }
 
    Ok(results)
 }



借用参照の制約

非同期Commandsでは、借用参照の使用に制約がある。

String vs &str の違い

非同期関数では、&str よりも String を使用することが推奨される。

  • 問題のある記述
     // これは動作しない可能性がある
     
     #[command]
     async fn process_text(text: &str) -> Result<String, String> {
        tokio::time::sleep(Duration::from_millis(100)).await;
        Ok(text.to_uppercase())
     }
    

  • 推奨される記述
     // Stringを使用する
     
     #[command]
     async fn process_text(text: String) -> Result<String, String> {
        tokio::time::sleep(Duration::from_millis(100)).await;
        Ok(text.to_uppercase())
     }
    


ライフタイムの考慮

非同期コンテキストでは、ライフタイムの管理が複雑になる。

 use serde::Deserialize;
 
 // データを所有する構造体
 #[derive(Deserialize)]
 struct ProcessRequest {
    input: String,         // Stringを使用
    options: Vec<String>,  // Vecを使用
 }
 
 #[command]
 async fn process_request(request: ProcessRequest) -> Result<String, String> {
    // 非同期処理内でデータを安全に使用できる
    let result = process_with_options(&request.input, &request.options).await?;
    Ok(result)
 }


所有権の移動パターン

非同期タスクにデータを渡す際は、所有権を移動する。

 #[command]
 async fn start_processing(data: Vec<u8>) -> Result<String, String> {
    // dataの所有権がこの関数に移動
 
    // tokio::spawnに移動
    let handle = tokio::spawn(async move {
       // dataの所有権がこのクロージャに移動
       process_bytes(data).await
    });
 
    let result = handle.await.map_err(|e| e.to_string())??;
    Ok(result)
 }


参照が必要な場合

どうしても参照が必要な場合は、Arc を使用する。

 use std::sync::Arc;
 use tokio::sync::RwLock;
 
 #[command]
 async fn shared_access(data_id: u32) -> Result<String, String> {
    // グローバル状態へのアクセス
    let state = get_app_state().await;
    let data = state.read().await;
 
    match data.get(&data_id) {
       Some(value) => Ok(value.clone()),
       None => Err(format!("Data {} not found", data_id)),
    }
 }



Channelによるストリーミング通信

Tauri v2では、Channel APIを使用してストリーミング通信を実現できる。

Channel APIの概要

Channelは、バックエンドからフロントエンドへのイベントストリームを提供する。

Channelの主な用途
用途 説明
プログレス更新 長時間処理の進捗をリアルタイムで通知
ログストリーミング バックエンドのログをフロントエンドに表示
イベント通知 ファイル変更、ネットワークイベント等の通知


イベントベースの通信

  • Rust側でのChannelの定義
     use tauri::ipc::Channel;
     use serde::Serialize;
     
     // イベント型の定義
     #[derive(Clone, Serialize)]
     enum DownloadEvent {
        Started { url: String, total_size: u64 },
        Progress { downloaded: u64, percentage: f64 },
        Finished { success: bool },
        Error { message: String },
     }
     
     #[command]
     async fn download_file(
        url: String,
        on_event: Channel<DownloadEvent>
     ) -> Result<(), String> {
        // 開始イベント
        on_event.send(DownloadEvent::Started {
           url: url.clone(),
           total_size: 0,
        }).map_err(|e| e.to_string())?;
     
        // ダウンロード処理のシミュレーション
        for i in 0..100 {
           tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
     
           on_event.send(DownloadEvent::Progress {
              downloaded: (i + 1) * 1024 * 1024,
              percentage: (i + 1) as f64,
           }).map_err(|e| e.to_string())?;
        }
     
        // 完了イベント
        on_event.send(DownloadEvent::Finished { success: true })
           .map_err(|e| e.to_string())?;
        
        Ok(())
     }
    


フロントエンドでのChannelの受信

  • TypeScript側での定義
     import { invoke, Channel } from '@tauri-apps/api/core'
     import { useState } from 'react'
     
     interface DownloadEvent {
       Started?: { url: string; total_size: number }
       Progress?: { downloaded: number; percentage: number }
       Finished?: { success: boolean }
       Error?: { message: string }
     }
     
     function DownloadComponent() {
       const [progress, setProgress] = useState(0)
       const [status, setStatus] = useState('idle')
       const [error, setError] = useState<string | null>(null)
     
       const handleDownload = async () => {
         setProgress(0)
         setStatus('downloading')
         setError(null)
     
         const channel = new Channel<DownloadEvent>()
     
         channel.onmessage = (event) => {
           if (event.Started) {
             setStatus('downloading')
             console.log('Download started:', event.Started.url)
           }
           else if (event.Progress) {
             setProgress(event.Progress.percentage)
           }
           else if (event.Finished) {
             setStatus('completed')
             console.log('Download finished:', event.Finished.success)
           }
           else if (event.Error) {
             setStatus('error')
             setError(event.Error.message)
           }
         }
     
         try {
           await invoke('download_file', {
             url: 'https://example.com/large-file.zip',
             onEvent: channel
           })
         }
         catch (err) {
           setStatus('error')
           setError(err as string)
         }
       }
     
       return (
         <div>
           <h2>File Download</h2>
           <button 
             onClick={handleDownload}
             disabled={status === 'downloading'}
           >
             {status === 'downloading' ? 'Downloading...' : 'Download'}
           </button>
     
           {status === 'downloading' && (
             <div>
               <progress value={progress} max={100} />
               <span>{progress.toFixed(1)}%</span>
             </div>
           )}
     
           {status === 'completed' && (
             <p style={{ color: 'green' }}>Download completed!</p>
           )}
     
           {error && (
             <p style={{ color: 'red' }}>Error: {error}</p>
           )}
         </div>
       )
     }
     
     export default DownloadComponent
    


双方向通信の定義

  • フロントエンドからバックエンドへのキャンセル通知
     use std::sync::Arc;
     use tokio::sync::atomic::{AtomicBool, Ordering};
     
     #[command]
     async fn cancellable_download(
        url: String,
        on_event: Channel<DownloadEvent>,
        cancel_flag: Arc<AtomicBool>
     ) -> Result<(), String> {
        on_event.send(DownloadEvent::Started {
           url: url.clone(),
           total_size: 0,
        }).map_err(|e| e.to_string())?;
     
        for i in 0..100 {
           // キャンセルチェック
           if cancel_flag.load(Ordering::Relaxed) {
              on_event.send(DownloadEvent::Finished { success: false })
                 .map_err(|e| e.to_string())?;
              return Ok(());
           }
     
           tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
     
           on_event.send(DownloadEvent::Progress {
              downloaded: (i + 1) * 1024 * 1024,
              percentage: (i + 1) as f64,
           }).map_err(|e| e.to_string())?;
        }
     
        on_event.send(DownloadEvent::Finished { success: true })
           .map_err(|e| e.to_string())?;
     
        Ok(())
     }
    



プログレス表示パターン

Channel APIを使用したプログレス表示の定義パターンを示す。

進捗更新の定義

  • 複数段階の処理の進捗を通知する例
     use serde::Serialize;
     
     #[derive(Clone, Serialize)]
     struct ProcessingProgress {
        stage: String,
        current: u32,
        total: u32,
        message: String,
     }
     
     #[command]
     async fn process_large_file(
        file_path: String,
        on_progress: Channel<ProcessingProgress>
     ) -> Result<String, String> {
     
        // Stage 1 : 読み込み
        on_progress.send(ProcessingProgress {
           stage: "reading".to_string(),
           current: 0,
           total: 3,
           message: "Reading file...".to_string(),
        }).map_err(|e| e.to_string())?;
     
        let content = tokio::fs::read_to_string(&file_path)
           .await
           .map_err(|e| format!("Failed to read file: {}", e))?;
     
        // Stage 2 : 処理
        on_progress.send(ProcessingProgress {
           stage: "processing".to_string(),
           current: 1,
           total: 3,
           message: "Processing content...".to_string(),
        }).map_err(|e| e.to_string())?;
     
        let lines: Vec<&str> = content.lines().collect();
        let total_lines = lines.len();
     
        for (i, line) in lines.iter().enumerate() {
           // 定期的な進捗更新
           if i % 1000 == 0 {
              on_progress.send(ProcessingProgress {
                 stage: "processing".to_string(),
                 current: 1,
                 total: 3,
                 message: format!("Processing line {}/{}", i, total_lines),
              }).map_err(|e| e.to_string())?;
           }
     
           // 行の処理
           process_line(line);
        }
     
        // Stage 3 : 書き込み
        on_progress.send(ProcessingProgress {
           stage: "writing".to_string(),
           current: 2,
           total: 3,
           message: "Writing output...".to_string(),
        }).map_err(|e| e.to_string())?;
     
        // 処理結果の書き込み...
     
        // 完了
        on_progress.send(ProcessingProgress {
           stage: "completed".to_string(),
           current: 3,
           total: 3,
           message: "Processing completed!".to_string(),
        }).map_err(|e| e.to_string())?;
     
        Ok("File processed successfully".to_string())
     }
     
     fn process_line(line: &str) {
        // 行の処理...
     }
    


フロントエンドでの表示

  • Reactでのプログレス表示コンポーネント
     import { useState } from 'react'
     import { invoke, Channel } from '@tauri-apps/api/core'
     
     interface ProcessingProgress {
       stage: string
       current: number
       total: number
       message: string
     }
     
     function FileProcessor() {
       const [progress, setProgress] = useState<ProcessingProgress | null>(null)
       const [isProcessing, setIsProcessing] = useState(false)
       const [logs, setLogs] = useState<string[]>([])
     
       const addLog = (message: string) => {
         setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`])
       }
     
       const processFile = async (filePath: string) => {
         setIsProcessing(true)
         setLogs([])
     
         const channel = new Channel<ProcessingProgress>()
     
         channel.onmessage = (event) => {
           setProgress(event)
           addLog(event.message)
         }
     
         try {
           const result = await invoke<string>('process_large_file', {
             filePath,
             onProgress: channel
           })
           addLog(`Result: ${result}`)
         }
         catch (error) {
           addLog(`Error: ${error}`)
         }
         finally {
           setIsProcessing(false)
         }
       }
     
       const handleFileSelect = async () => {
         // ファイル選択ダイアログを開く
         const filePath = await invoke<string>('select_file')
         if (filePath) {
           await processFile(filePath)
         }
       }
     
       return (
         <div style={{ padding: '20px' }}>
           <h2>File Processor</h2>
     
           <button 
             onClick={handleFileSelect}
             disabled={isProcessing}
           >
             {isProcessing ? 'Processing...' : 'Select File'}
           </button>
     
           {progress && (
             <div style={{ marginTop: '20px' }}>
               <h3>Progress</h3>
               <p>Stage: {progress.stage}</p>
               <p>Step: {progress.current}/{progress.total}</p>
               <progress 
                 value={progress.current} 
                 max={progress.total}
                 style={{ width: '100%' }}
               />
               <p>{progress.message}</p>
             </div>
           )}
     
           <div style={{ marginTop: '20px' }}>
             <h3>Log</h3>
             <div
               style={{
                 height: '200px',
                 overflow: 'auto',
                 backgroundColor: '#f5f5f5',
                 padding: '10px',
                 fontFamily: 'monospace'
               }}
             >
               {logs.map((log, index) => (
                 <div key={index}>{log}</div>
               ))}
             </div>
           </div>
         </div>
       )
     }
     
     export default FileProcessor
    


キャンセル処理

  • 処理のキャンセルを定義する例
     import { useState, useRef } from 'react'
     import { invoke, Channel } from '@tauri-apps/api/core'
     
     interface DownloadProgress {
       percentage: number
       speed: string
       eta: string
     }
     
     function DownloadWithCancel() {
       const [progress, setProgress] = useState<DownloadProgress | null>(null)
       const [isDownloading, setIsDownloading] = useState(false)
       const cancelRef = useRef<boolean>(false)
     
       const startDownload = async (url: string) => {
         setIsDownloading(true)
         cancelRef.current = false
         setProgress(null)
     
         const channel = new Channel<DownloadProgress>()
     
         channel.onmessage = (event) => {
           if (!cancelRef.current) {
             setProgress(event)
           }
         }
     
         try {
           await invoke('download_with_cancel', {
             url,
             onProgress: channel,
             cancelToken: cancelRef.current
           })
         }
         catch (error) {
           if (!cancelRef.current) {
             console.error('Download error:', error)
           }
         }
         finally {
           setIsDownloading(false)
         }
       }
     
       const cancelDownload = () => {
         cancelRef.current = true
         setProgress(null)
         setIsDownloading(false)
       }
     
       return (
         <div>
           <button
             onClick={() => startDownload('https://example.com/file.zip')}
             disabled={isDownloading}
           >
             Start Download
           </button>
     
           {isDownloading && (
             <button onClick={cancelDownload} style={{ marginLeft: '10px' }}>
               Cancel
             </button>
           )}
     
           {progress && (
             <div style={{ marginTop: '10px' }}>
               <progress value={progress.percentage} max={100} />
               <p>{progress.percentage.toFixed(1)}%</p>
               <p>Speed: {progress.speed}</p>
               <p>ETA: {progress.eta}</p>
             </div>
           )}
         </div>
       )
     }
     
     export default DownloadWithCancel
    



サンプルコード

ファイルダウンロードの進捗表示

  • Rust側の定義
     use serde::Serialize;     // JSONシリアライズ機能を提供
     use tauri::ipc::Channel;  // TauriのIPCチャネル機能 (フロントエンドとのリアルタイム通信用)
     
     // ダウンロード状態を表す列挙型
     #[derive(Clone, Serialize)]
     pub enum DownloadStatus {
        Idle,                           // アイドル状態 (ダウンロード未開始)
        Downloading { progress: f64 },  // ダウンロード中 (進捗率: 0.0〜100.0)
        Completed,                      // ダウンロード完了
        Failed { error: String },       // ダウンロード失敗 (エラーメッセージ付き)
     }
     
     // ダウンロード進捗情報を格納する構造体
     #[derive(Clone, Serialize)]
     pub struct DownloadProgress {
        pub status: DownloadStatus,  // 現在の状態
        pub bytes_downloaded: u64,   // ダウンロード済みバイト数
        pub total_bytes: u64,        // 総バイト数
        pub filename: String,        // ファイル名
     }
     
     // Tauriコマンドとしてマーク (フロントエンドから呼び出し可能)
     #[command]
     pub async fn download_with_progress(
        url: String,                            // ダウンロード対象のURL
        on_progress: Channel<DownloadProgress>  // 進捗通知用チャネル
     ) -> Result<String, String> {
        // URLからファイル名を抽出 (最後のスラッシュ以降を取得)
        let filename = url.split('/').last().unwrap_or("download").to_string();
     
        // ダウンロード開始を通知 (進捗0[%])
        on_progress.send(DownloadProgress {
           status: DownloadStatus::Downloading { progress: 0.0 },
           bytes_downloaded: 0,
           total_bytes: 0,
           filename: filename.clone(),
        }).map_err(|e| e.to_string())?;  // 送信エラーを文字列に変換
     
        // ダウンロード処理のシミュレーション
        let total_size = 10 * 1024 * 1024;  // 総ファイルサイズ: 10[MB]
        let chunk_size = 100 * 1024;        // チャンクサイズ: 100[KB]
        let mut downloaded = 0u64;          // ダウンロード済みバイト数
     
        // チャンクごとにダウンロードをシミュレート
        while downloaded < total_size {
           // 50ミリ秒待機 (実際のダウンロードの遅延をシミュレート)
           tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
     
           // ダウンロード済みバイト数を更新 (総サイズを超えないように制限)
           downloaded = (downloaded + chunk_size).min(total_size);
     
           // 進捗率を計算(0.0〜100.0の範囲)
           let progress = (downloaded as f64 / total_size as f64) * 100.0;
     
           // 現在の進捗をフロントエンドに通知
           on_progress.send(DownloadProgress {
              status: DownloadStatus::Downloading { progress },
              bytes_downloaded: downloaded,
              total_bytes: total_size,
              filename: filename.clone(),
           }).map_err(|e| e.to_string())?;  // 送信エラーを文字列に変換
        }
     
        // ダウンロード完了を通知
        on_progress.send(DownloadProgress {
           status: DownloadStatus::Completed,
           bytes_downloaded: total_size,
           total_bytes: total_size,
           filename: filename.clone(),
        }).map_err(|e| e.to_string())?;  // 送信エラーを文字列に変換
     
        // 成功メッセージを返す
        Ok(format!("Downloaded: {}", filename))
    }
    

  • TypeScript / React側の定義
     import { useState } from 'react'                        // ReactのuseStateフックをインポート (状態管理用)
     import { invoke, Channel } from '@tauri-apps/api/core'  // Tauri APIからinvoke (コマンド実行) と Channel (リアルタイム通信) をインポート
     
     // ダウンロード進捗情報の型定義
     interface DownloadProgress {
       status: {
         Downloading?: { progress: number }  // ダウンロード中 (進捗率付き)
         Completed?: null                    // 完了状態
         Failed?: { error: string }          // 失敗状態 (エラーメッセージ付き)
       }
       bytes_downloaded: number               // ダウンロード済みバイト数
       total_bytes: number                    // 総バイト数
       filename: string                       // ファイル名
     }
     
     // ダウンロードマネージャーコンポーネント
     function DownloadManager() {
       // ダウンロード進捗情報を管理する状態 (URLをキーとするMap)
       const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map())
     
       // バイト数を人間が読みやすい形式に変換する関数
       const formatBytes = (bytes: number): string => {
         if (bytes < 1024) return `${bytes} B`                              // バイト単位
         if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`  // キロバイト単位
         return `${(bytes / (1024 * 1024)).toFixed(2)} MB`                  // メガバイト単位
       }
     
       // ダウンロードを開始する非同期関数
       const startDownload = async (url: string) => {
         // 進捗通知を受け取るチャネルを作成
         const channel = new Channel<DownloadProgress>()
     
         // チャネルからメッセージを受信した時の処理
         channel.onmessage = (event) => {
           // 状態を更新(該当URLの進捗情報を更新)
           setDownloads((prev) => {
             const newMap = new Map(prev)  // 既存のMapをコピー
             newMap.set(url, event)        // 新しい進捗情報を設定
             return newMap                 // 新しいMapを返す
           })
         }
     
         // Rust側のdownload_with_progressコマンドを呼び出し
         invoke('download_with_progress', { url, onProgress: channel })
           .catch(console.error)  // エラー時はコンソールに出力
       }
     
       return (
         <div style={{ padding: '20px' }}>
           <h2>Download Manager</h2>
     
           {/* ダウンロードボタンエリア */}
           <div style={{ marginBottom: '20px' }}>
             {/* file1.zipのダウンロードボタン */}
             <button onClick={() => startDownload('https://example.com/file1.zip')}>
               Download file1.zip
             </button>
             {/* file2.zipのダウンロードボタン */}
             <button 
               onClick={() => startDownload('https://example.com/file2.zip')}
               style={{ marginLeft: '10px' }}
             >
               Download file2.zip
             </button>
           </div>
     
           {/* ダウンロード進捗リスト */}
           <div>
             {/* Mapのエントリを配列に変換してマップ */}
             {Array.from(downloads.entries()).map(([url, progress]) => (
               <div
                 key={url}
                 style={{
                   border: '1px solid #ccc',
                   padding: '10px',
                   marginBottom: '10px',
                   borderRadius: '4px'
                 }}
               >
                 {/* ファイル名表示 */}
                 <h4>{progress.filename}</h4>
     
                 {/* ダウンロード中の表示 */}
                 {progress.status.Downloading && (
                   <>
                     {/* プログレスバー */}
                     <progress
                       value={progress.status.Downloading.progress}
                       max={100}
                       style={{ width: '100%' }}
                     />
                     {/* 進捗テキスト */}
                     <p>
                       {progress.status.Downloading.progress.toFixed(1)}% - 
                       {formatBytes(progress.bytes_downloaded)} / {formatBytes(progress.total_bytes)}
                     </p>
                   </>
                 )}
     
                 {/* 完了状態の表示 */}
                 {progress.status.Completed && (
                   <p style={{ color: 'green' }}>Download completed!</p>
                 )}
     
                 {/* 失敗状態の表示 */}
                 {progress.status.Failed && (
                   <p style={{ color: 'red' }}>Error: {progress.status.Failed.error}</p>
                 )}
               </div>
             ))}
           </div>
         </div>
       )
     }
     
     // コンポーネントをエクスポート
     export default DownloadManager
    



推奨される事柄

非同期エラーの適切な処理

非同期処理では、エラーが発生しても適切にハンドリングする必要がある。

 // Tauriコマンドとしてマーク (フロントエンドから呼び出し可能)
 #[command]
 async fn safe_async_operation() -> Result<String, String> {
    // ファイルを非同期で読み込む
    let result = tokio::fs::read_to_string("config.json")
       .await  // 非同期処理の完了を待機
       // ファイル読み込みエラーをカスタムエラーメッセージに変換
       .map_err(|e| format!("Failed to read config: {}", e))?;
 
    // JSON文字列をConfig構造体にパース
    let config: Config = serde_json::from_str(&result)
       // JSONパースエラーをカスタムエラーメッセージに変換
       .map_err(|e| format!("Failed to parse config: {}", e))?;
 
    // 成功時は設定のnameフィールドを返す
    Ok(config.name)
 }


リソースのクリーンアップ

非同期タスクが中断された場合でも、リソースを適切に解放する。

 use tokio::io::AsyncWriteExt;
 
 #[command]
 async fn write_file_safe(path: String, content: String) -> Result<(), String> {
    let mut file = tokio::fs::File::create(&path)
       .await
       .map_err(|e| format!("Failed to create file: {}", e))?;
 
    // 書き込みに失敗した場合でも、ファイルを確実にフラッシュする
    let result = file.write_all(content.as_bytes()).await;
 
    // 常にフラッシュする
    let _ = file.flush().await;
 
    result.map_err(|e| format!("Failed to write: {}", e))
 }


タイムアウトの設定

長時間実行される可能性のある処理にはタイムアウトを設定する。

 // 非同期タイムアウト機能を提供するTokioの時間関連モジュールをインポート
 use tokio::time::{timeout, Duration};
 
 // Tauriコマンドとしてマーク(フロントエンドから呼び出し可能)
 #[command]
 async fn fetch_with_timeout(url: String) -> Result<String, String> {
    // 30秒のタイムアウトを設定してHTTP GETリクエストを実行
    let result = timeout(
       Duration::from_secs(30),  // 最大待機時間 : 30秒
       reqwest::get(&url)        // 指定URLへのGETリクエスト
    )
    .await  // 非同期処理の完了を待機
    // タイムアウトが発生した場合のエラーハンドリング
    .map_err(|_| "Request timed out".to_string())?    // タイムアウト時はエラーメッセージを返す
    // HTTPリクエスト自体が失敗した場合のエラーハンドリング
    .map_err(|e| format!("Request failed: {}", e))?;  // 失敗理由を含むエラーメッセージを返す
 
    // レスポンスボディをテキストとして取得
    let body = result.text()
       .await  // 非同期でテキスト読み込みを待機
       // テキスト読み込みに失敗した場合のエラーハンドリング
       .map_err(|e| format!("Failed to read response: {}", e))?;
 
    // 成功時はレスポンスボディを返す
    Ok(body)
 }


同時実行数の制限

リソースを消費する処理は、同時実行数を制限する。

 use tokio::sync::Semaphore;
 use std::sync::Arc;
 
 static SEMAPHORE: once_cell::sync::Lazy<Arc<Semaphore>> = 
    once_cell::sync::Lazy::new(|| Arc::new(Semaphore::new(3)));
 
 #[command]
 async fn limited_operation(id: u32) -> Result<String, String> {
    let permit = SEMAPHORE.acquire()
       .await
       .map_err(|e| format!("Semaphore error: {}", e))?;
 
    // 最大3つのタスクが同時実行
    let result = expensive_operation(id).await?;
 
    drop(permit);  // 明示的に解放
    Ok(result)
 }



関連情報