460 lines
9.9 KiB
Markdown
460 lines
9.9 KiB
Markdown
# 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<string>('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<number>('calculate', {
|
|
a: 10,
|
|
b: 5,
|
|
operation: 'add'
|
|
});
|
|
```
|
|
|
|
### Async Command
|
|
|
|
**Rust:**
|
|
```rust
|
|
#[tauri::command]
|
|
async fn fetch_data(url: String) -> Result<String, String> {
|
|
// 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<string>('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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where S: serde::ser::Serializer {
|
|
serializer.serialize_str(self.to_string().as_ref())
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn read_config(path: String) -> Result<Config, AppError> {
|
|
if !std::path::Path::new(&path).exists() {
|
|
return Err(AppError::NotFound(path));
|
|
}
|
|
// ...
|
|
}
|
|
```
|
|
|
|
**Frontend:**
|
|
```typescript
|
|
try {
|
|
const config = await invoke<Config>('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<String>,
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_count(state: State<'_, Mutex<AppState>>) -> u32 {
|
|
state.lock().unwrap().counter
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn increment(state: State<'_, Mutex<AppState>>) -> u32 {
|
|
let mut s = state.lock().unwrap();
|
|
s.counter += 1;
|
|
s.counter
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn add_item(item: String, state: State<'_, Mutex<AppState>>) {
|
|
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<Response, String> {
|
|
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<ArrayBuffer>('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<number>('progress', (event) => {
|
|
console.log(`Progress: ${event.payload}%`);
|
|
});
|
|
|
|
// Listen once
|
|
await once<string>('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<String>,
|
|
on_progress: Channel<ProgressUpdate>
|
|
) -> 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<ProgressUpdate>();
|
|
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<DownloadEvent>
|
|
) -> Result<String, String> {
|
|
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<DownloadEvent>();
|
|
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<string>('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
|