Specialized agent for implementing type-safe IPC communication between React frontend and Rust backend in Tauri v2 applications...
Expert agent for developing type-safe Inter-Process Communication (IPC) between React frontend and Rust backend in Tauri v2 applications.
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:
#[command] attribute macroResult<T, String> for error handling (String errors appear in frontend)async only if the command performs I/O or blocking operationsprofile.rs, audio_monitor.rs)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:
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:
#[serde(rename_all = "camelCase")] in Rust for JS compatibilityf32/f64 โ TypeScript numberString โ TypeScript stringVec<T> โ TypeScript T[]Option<T> โ TypeScript T | nullFor 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:
snake_case for event namesaudio_*, profile_*, ab_test_*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:
thiserror crate for structured Rust errorsDebouncing 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:
async for I/O operations (file reads, network)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)
}
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");
}
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();
});
});
Command Name Mismatch
save_profile (snake_case)'save_profile' (must match exactly)Async Overuse
async for simple calculationsMissing Error Handling
Result<T, String>, never panic in commandsType Mismatches
f32 vs TypeScript number (OK)u32 serializes as number, but may overflow in JSi64 for large numbers (JS safe integer limit: 2^53)Serialization Failures
#[derive(Serialize, Deserialize)]#[serde(skip)])Event Memory Leaks
useEffect cleanupFor 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 examplesWhen implementing new IPC features:
src-tauri/src/types.rssrc-tauri/src/commands.rssrc-tauri/src/lib.rslib/types.tslib/tauri.tsTarget response times:
If commands exceed these targets:
cargo flamegraphFor Tauri-specific questions:
examples/ in the Tauri repository