# Tauri v2 IPC Patterns Reference ## Overview Tauri v2 provides three IPC primitives: 1. **Commands**: Request-response (most common) 2. **Events**: Fire-and-forget notifications 3. **Channels**: High-frequency streaming ## Commands (invoke) ### Basic Command **Rust:** ```rust #[tauri::command] fn greet(name: String) -> String { format!("Hello, {}!", name) } // Register in builder tauri::Builder::default() .invoke_handler(tauri::generate_handler![greet]) ``` **Frontend:** ```typescript import { invoke } from '@tauri-apps/api/core'; const result = await invoke('greet', { name: 'World' }); ``` ### Command with Multiple Arguments **Rust:** ```rust #[tauri::command] fn calculate(a: i32, b: i32, operation: String) -> i32 { match operation.as_str() { "add" => a + b, "sub" => a - b, "mul" => a * b, "div" => a / b, _ => 0, } } ``` **Frontend:** ```typescript const result = await invoke('calculate', { a: 10, b: 5, operation: 'add' }); ``` ### Async Command **Rust:** ```rust #[tauri::command] async fn fetch_data(url: String) -> Result { // Use owned types (String, not &str) in async commands let response = reqwest::get(&url) .await .map_err(|e| e.to_string())?; response.text() .await .map_err(|e| e.to_string()) } ``` **Frontend:** ```typescript try { const data = await invoke('fetch_data', { url: 'https://api.example.com' }); } catch (error) { console.error('Failed:', error); } ``` ### Command with Result Error Handling **Rust:** ```rust use thiserror::Error; #[derive(Debug, Error)] enum AppError { #[error("File not found: {0}")] NotFound(String), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Permission denied")] PermissionDenied, } impl serde::Serialize for AppError { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer { serializer.serialize_str(self.to_string().as_ref()) } } #[tauri::command] fn read_config(path: String) -> Result { if !std::path::Path::new(&path).exists() { return Err(AppError::NotFound(path)); } // ... } ``` **Frontend:** ```typescript try { const config = await invoke('read_config', { path: '/config.json' }); } catch (error) { // error is the serialized error string console.error('Config error:', error); } ``` ### Command with State **Rust:** ```rust use std::sync::Mutex; use tauri::State; struct AppState { counter: u32, items: Vec, } #[tauri::command] fn get_count(state: State<'_, Mutex>) -> u32 { state.lock().unwrap().counter } #[tauri::command] fn increment(state: State<'_, Mutex>) -> u32 { let mut s = state.lock().unwrap(); s.counter += 1; s.counter } #[tauri::command] fn add_item(item: String, state: State<'_, Mutex>) { state.lock().unwrap().items.push(item); } // In builder: tauri::Builder::default() .manage(Mutex::new(AppState { counter: 0, items: vec![] })) .invoke_handler(tauri::generate_handler![get_count, increment, add_item]) ``` ### Command with Window Access **Rust:** ```rust use tauri::{WebviewWindow, AppHandle}; #[tauri::command] fn get_window_info(window: WebviewWindow) -> String { format!("Window label: {}", window.label()) } #[tauri::command] fn create_window(app: AppHandle) -> Result<(), String> { tauri::WebviewWindowBuilder::new( &app, "new-window", tauri::WebviewUrl::App("index.html".into()) ) .title("New Window") .build() .map_err(|e| e.to_string())?; Ok(()) } ``` ### Command with Raw Binary Data **Rust:** ```rust use tauri::ipc::Response; #[tauri::command] fn read_binary_file(path: String) -> Result { let data = std::fs::read(&path).map_err(|e| e.to_string())?; Ok(Response::new(data)) // Avoids JSON serialization overhead } #[tauri::command] fn upload_file(request: tauri::ipc::Request) -> Result<(), String> { let tauri::ipc::InvokeBody::Raw(data) = request.body() else { return Err("Expected raw body".into()); }; std::fs::write("upload.bin", data).map_err(|e| e.to_string()) } ``` **Frontend:** ```typescript // Reading binary const data = await invoke('read_binary_file', { path: '/file.bin' }); // Uploading binary const fileData = new Uint8Array([1, 2, 3, 4]); await invoke('upload_file', fileData); ``` --- ## Events ### Emit from Rust to Frontend **Rust:** ```rust use tauri::Emitter; #[tauri::command] fn start_background_task(app: tauri::AppHandle) { std::thread::spawn(move || { for i in 0..100 { std::thread::sleep(std::time::Duration::from_millis(100)); app.emit("progress", i).unwrap(); } app.emit("complete", "Task finished").unwrap(); }); } // Emit to specific window #[tauri::command] fn notify_window(app: tauri::AppHandle, window_label: String, message: String) { app.emit_to(&window_label, "notification", message).unwrap(); } ``` **Frontend:** ```typescript import { listen, once } from '@tauri-apps/api/event'; // Listen continuously const unlisten = await listen('progress', (event) => { console.log(`Progress: ${event.payload}%`); }); // Listen once await once('complete', (event) => { console.log(event.payload); }); // Clean up when done unlisten(); ``` ### Emit from Frontend to Rust **Frontend:** ```typescript import { emit } from '@tauri-apps/api/event'; await emit('user-action', { action: 'click', target: 'button' }); ``` **Rust (in setup or command):** ```rust use tauri::Listener; fn setup_listeners(app: &tauri::App) { app.listen("user-action", |event| { println!("User action: {:?}", event.payload()); }); } ``` ### Window-Specific Events **Rust:** ```rust use tauri::{Emitter, WebviewWindow}; #[tauri::command] fn emit_to_window(window: WebviewWindow, message: String) { window.emit("window-message", message).unwrap(); } ``` --- ## Channels (Streaming) ### Basic Channel Pattern **Rust:** ```rust use tauri::ipc::Channel; #[derive(Clone, serde::Serialize)] struct ProgressUpdate { current: u32, total: u32, message: String, } #[tauri::command] async fn process_files( files: Vec, on_progress: Channel ) -> Result<(), String> { let total = files.len() as u32; for (i, file) in files.iter().enumerate() { // Process file... on_progress.send(ProgressUpdate { current: i as u32 + 1, total, message: format!("Processing {}", file), }).unwrap(); } Ok(()) } ``` **Frontend:** ```typescript import { invoke, Channel } from '@tauri-apps/api/core'; interface ProgressUpdate { current: number; total: number; message: string; } const channel = new Channel(); channel.onmessage = (update) => { const percent = (update.current / update.total) * 100; console.log(`${percent}% - ${update.message}`); }; await invoke('process_files', { files: ['file1.txt', 'file2.txt'], onProgress: channel }); ``` ### Tagged Union Events (Discriminated) **Rust:** ```rust use tauri::ipc::Channel; #[derive(Clone, serde::Serialize)] #[serde(tag = "event", content = "data")] enum DownloadEvent { Started { url: String, size: u64 }, Progress { downloaded: u64, total: u64 }, Complete { path: String }, Error { message: String }, } #[tauri::command] async fn download_file( url: String, on_event: Channel ) -> Result { on_event.send(DownloadEvent::Started { url: url.clone(), size: 1000, }).unwrap(); for i in 0..=100 { on_event.send(DownloadEvent::Progress { downloaded: i * 10, total: 1000, }).unwrap(); tokio::time::sleep(std::time::Duration::from_millis(10)).await; } let path = "/downloads/file.zip".to_string(); on_event.send(DownloadEvent::Complete { path: path.clone(), }).unwrap(); Ok(path) } ``` **Frontend:** ```typescript import { invoke, Channel } from '@tauri-apps/api/core'; type DownloadEvent = | { event: 'Started'; data: { url: string; size: number } } | { event: 'Progress'; data: { downloaded: number; total: number } } | { event: 'Complete'; data: { path: string } } | { event: 'Error'; data: { message: string } }; const channel = new Channel(); channel.onmessage = (msg) => { switch (msg.event) { case 'Started': console.log(`Starting download: ${msg.data.url} (${msg.data.size} bytes)`); break; case 'Progress': const percent = (msg.data.downloaded / msg.data.total) * 100; console.log(`Download: ${percent.toFixed(1)}%`); break; case 'Complete': console.log(`Downloaded to: ${msg.data.path}`); break; case 'Error': console.error(`Download failed: ${msg.data.message}`); break; } }; const path = await invoke('download_file', { url: 'https://example.com/file.zip', onEvent: channel }); ``` --- ## IPC Selection Guide | Pattern | Use Case | Direction | Frequency | |---------|----------|-----------|-----------| | **Commands** | Request-response, data fetching | Frontend → Rust | One-time | | **Events** | Notifications, state changes | Bidirectional | Low-medium | | **Channels** | Progress updates, streaming data | Rust → Frontend | High | ### When to Use Each **Commands (invoke)** - Fetching data from Rust - Performing actions that return results - CRUD operations - Most common pattern **Events (emit/listen)** - Notifying UI of background changes - Broadcasting to multiple windows - Fire-and-forget notifications - System events (window close, minimize) **Channels** - File download/upload progress - Long-running operations with updates - Streaming log output - Real-time data feeds