Agent skill
tauri-ipc-developer
Specialized agent for implementing type-safe IPC communication between React frontend and Rust backend in Tauri v2 applications. Use when adding new Tauri commands, implementing bidirectional events, debugging IPC serialization issues, or optimizing command performance.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/tauri-ipc-developer-iammarkps-eqapo-gui
SKILL.md
Tauri IPC Developer
Expert agent for developing type-safe Inter-Process Communication (IPC) between React frontend and Rust backend in Tauri v2 applications.
Core Responsibilities
1. Implement New Tauri Commands
When adding new backend functionality accessible from the frontend:
Rust Side (src-tauri/src/commands.rs):
use tauri::command;
use crate::types::ParametricBand;
use crate::profile::ProfileManager;
#[command]
pub async fn save_profile(
name: String,
bands: Vec<ParametricBand>,
preamp: f32,
) -> Result<String, String> {
// Implementation
match ProfileManager::save(&name, bands, preamp) {
Ok(path) => Ok(path.to_string_lossy().to_string()),
Err(e) => Err(format!("Failed to save profile: {}", e)),
}
}
Key Requirements:
- Use
#[command]attribute macro - Return
Result<T, String>for error handling (String errors appear in frontend) - Use
asynconly if the command performs I/O or blocking operations - Keep command functions thin - delegate to modules (
profile.rs,audio_monitor.rs) - Handle all error cases explicitly
Frontend Side (lib/tauri.ts):
import { invoke } from '@tauri-apps/api/core';
import type { ParametricBand } from './types';
export async function saveProfile(
name: string,
bands: ParametricBand[],
preamp: number
): Promise<string> {
return await invoke<string>('save_profile', { name, bands, preamp });
}
Type Safety Checklist:
- ✅ TypeScript types match Rust struct definitions
- ✅ Error handling with try/catch in frontend
- ✅ Proper serialization of complex types (Vec, HashMap, Option)
- ✅ Command name matches exactly (snake_case)
2. Type Synchronization
Critical: Frontend and backend types MUST stay in sync.
Rust Types (src-tauri/src/types.rs):
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParametricBand {
pub filter_type: FilterType,
pub frequency: f32,
pub gain: f32,
pub q_factor: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FilterType {
Peaking,
LowShelf,
HighShelf,
}
TypeScript Types (lib/types.ts):
export interface ParametricBand {
filterType: 'Peaking' | 'LowShelf' | 'HighShelf';
frequency: number;
gain: number;
qFactor: number;
}
export type FilterType = 'Peaking' | 'LowShelf' | 'HighShelf';
Synchronization Rules:
- Use
#[serde(rename_all = "camelCase")]in Rust for JS compatibility - Rust
f32/f64→ TypeScriptnumber - Rust
String→ TypeScriptstring - Rust
Vec<T>→ TypeScriptT[] - Rust
Option<T>→ TypeScriptT | null - Rust enums → TypeScript union types
3. Bidirectional Events
For backend → frontend communication (e.g., audio peak meter updates):
Rust Emitter (src-tauri/src/audio_monitor.rs):
use tauri::{AppHandle, Emitter};
use serde::{Serialize, Deserialize};
#[derive(Clone, Serialize, Deserialize)]
pub struct PeakMeterUpdate {
pub peak_db: f32,
pub device_name: String,
pub sample_rate: u32,
}
pub fn emit_peak_update(app: &AppHandle, update: PeakMeterUpdate) {
let _ = app.emit("peak_meter_update", update);
}
Frontend Listener (lib/use-audio-status.ts):
import { listen } from '@tauri-apps/api/event';
import { useEffect, useState } from 'react';
interface PeakMeterUpdate {
peakDb: number;
deviceName: string;
sampleRate: number;
}
export function useAudioStatus() {
const [peakData, setPeakData] = useState<PeakMeterUpdate | null>(null);
useEffect(() => {
const unlisten = listen<PeakMeterUpdate>('peak_meter_update', (event) => {
setPeakData(event.payload);
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
return peakData;
}
Event Naming Convention:
- Use
snake_casefor event names - Prefix domain:
audio_*,profile_*,ab_test_* - Document event payloads in both codebases
4. Error Handling Patterns
Rust Command Error Handling:
#[command]
pub async fn load_profile(name: String) -> Result<EqProfile, String> {
ProfileManager::load(&name)
.map_err(|e| match e.kind() {
ErrorKind::NotFound => format!("Profile '{}' not found", name),
ErrorKind::PermissionDenied => "Permission denied".to_string(),
_ => format!("Failed to load profile: {}", e),
})
}
Frontend Error Handling:
try {
const profile = await loadProfile(name);
setCurrentProfile(profile);
} catch (error) {
console.error('Load failed:', error);
toast.error(error as string); // Tauri errors are strings
}
Error Best Practices:
- Return user-friendly error messages from Rust
- Log detailed errors server-side before converting to strings
- Use
thiserrorcrate for structured Rust errors - Catch and display errors in UI (toast notifications)
5. Performance Optimization
Debouncing Frequent Commands:
For real-time EQ adjustments, debounce on frontend:
import { debounce } from 'lodash-es';
const debouncedApply = useMemo(
() =>
debounce(async (bands: ParametricBand[], preamp: number) => {
await applyProfile(bands, preamp);
}, 250),
[]
);
useEffect(() => {
debouncedApply(bands, preamp);
}, [bands, preamp]);
Batching Updates:
Send multiple changes in one IPC call instead of multiple:
// ❌ Bad: 3 IPC calls
await updatePreamp(preamp);
await updateBands(bands);
await saveSettings();
// ✅ Good: 1 IPC call
await updateSettings({ preamp, bands, autoSave: true });
Async vs Sync Commands:
- Use
asyncfor I/O operations (file reads, network) - Use sync for CPU-bound operations < 16ms (audio math)
- Never block the main thread for > 16ms
6. Security Considerations
Input Validation:
Always validate on the Rust side:
#[command]
pub fn set_frequency(band_id: usize, freq: f32) -> Result<(), String> {
if !(20.0..=20000.0).contains(&freq) {
return Err("Frequency must be between 20 and 20000 Hz".to_string());
}
if band_id >= MAX_BANDS {
return Err(format!("Band ID {} exceeds maximum {}", band_id, MAX_BANDS));
}
// Safe to proceed
Ok(())
}
Path Traversal Prevention:
use std::path::PathBuf;
#[command]
pub fn load_profile_by_path(path: String) -> Result<EqProfile, String> {
let profile_dir = ProfileManager::get_profile_dir()?;
let requested_path = PathBuf::from(&path);
// Prevent path traversal attacks
if !requested_path.starts_with(&profile_dir) {
return Err("Invalid profile path".to_string());
}
ProfileManager::load_from_path(requested_path)
}
Command Registration
All commands must be registered in src-tauri/src/lib.rs:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
commands::apply_profile,
commands::save_profile,
commands::load_profile,
commands::list_profiles,
commands::delete_profile,
commands::get_settings,
commands::update_settings,
commands::import_eapo_config,
commands::export_eapo_config,
// Add new commands here
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Testing IPC Commands
Unit Tests (Rust):
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_save_profile() {
let result = save_profile(
"Test Profile".to_string(),
vec![],
0.0
).await;
assert!(result.is_ok());
}
}
Integration Tests (Frontend):
import { describe, it, expect } from 'vitest';
import { saveProfile } from './tauri';
describe('Tauri IPC', () => {
it('should save profile', async () => {
const result = await saveProfile('Test', [], 0);
expect(result).toBeDefined();
});
});
Common Pitfalls
-
Command Name Mismatch
- Rust:
save_profile(snake_case) - Frontend:
'save_profile'(must match exactly)
- Rust:
-
Async Overuse
- Don't use
asyncfor simple calculations - Only for I/O or operations > 16ms
- Don't use
-
Missing Error Handling
- Always return
Result<T, String>, never panic in commands - Handle all error branches
- Always return
-
Type Mismatches
- Rust
f32vs TypeScriptnumber(OK) - Rust
u32serializes as number, but may overflow in JS - Use
i64for large numbers (JS safe integer limit: 2^53)
- Rust
-
Serialization Failures
- Missing
#[derive(Serialize, Deserialize)] - Circular references (use
#[serde(skip)]) - Non-serializable types (file handles, closures)
- Missing
-
Event Memory Leaks
- Always unlisten in
useEffectcleanup - Remove listeners when components unmount
- Always unlisten in
Reference Materials
For detailed examples and patterns, see:
references/command_patterns.md- Common IPC command patternsreferences/type_mappings.md- Rust ↔ TypeScript type referencereferences/event_patterns.md- Event-driven communication examples
Development Workflow
When implementing new IPC features:
- Define Rust types in
src-tauri/src/types.rs - Implement command in
src-tauri/src/commands.rs - Register command in
src-tauri/src/lib.rs - Mirror types in
lib/types.ts - Create wrapper in
lib/tauri.ts - Test with curl or Tauri dev tools
- Integrate in React components
- Add error handling throughout the stack
Performance Benchmarks
Target response times:
- Simple queries (get settings): < 1ms
- File I/O (load profile): < 10ms
- Config write (apply EQ): < 50ms
- Heavy computation (FFT analysis): < 100ms
If commands exceed these targets:
- Profile with
cargo flamegraph - Consider caching frequently accessed data
- Move heavy work to separate threads
- Use streaming for large data sets
Support
For Tauri-specific questions:
- Tauri v2 Docs
- Tauri Discord
- Check
examples/in the Tauri repository
Didn't find tool you were looking for?