Tauriの基礎 - 非同期Commands
概要
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ランタイム上で実行される。
| 機能 | 説明 |
|---|---|
| 非同期タスク | 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は、バックエンドからフロントエンドへのイベントストリームを提供する。
| 用途 | 説明 |
|---|---|
| プログレス更新 | 長時間処理の進捗をリアルタイムで通知 |
| ログストリーミング | バックエンドのログをフロントエンドに表示 |
| イベント通知 | ファイル変更、ネットワークイベント等の通知 |
イベントベースの通信
- 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)
}
関連情報
- Tauriの基礎 - Commands
- Commandsの基本的な使用方法について
- Tauriの基礎 - Commandsのエラーハンドリング
- エラー処理、thiserror、anyhowについて
- Tauri公式ドキュメント - Commands
- 公式のCommandsリファレンス
- tokio公式チュートリアル
- tokioの詳細な使用方法
- tokio APIドキュメント
- tokioのAPIリファレンス