How the Meetily Frontend Invokes Tauri Commands and Listens for Rust Events
Meetily's Next.js frontend communicates with its Rust backend using Tauri's invoke API to execute commands and the listen API to subscribe to events, enabling real-time bidirectional data flow between the React UI and native audio processing code.
The Meetily desktop application bridges a Next.js React interface with a high-performance Rust core using Tauri's inter-process communication layer. Understanding how the frontend invokes Tauri commands and listens for events from Rust reveals the architecture that keeps the UI responsive while handling compute-intensive tasks like audio capture, VAD processing, and Whisper transcription.
Invoking Tauri Commands from TypeScript
The frontend centralizes all Rust command calls inside service modules. In frontend/src/services/recordingService.ts, the application imports ** invoke ** from @tauri-apps/api/core and exposes thin async wrappers that forward arguments to the Rust side.
Every command registered in the Tauri handler can be triggered using the string identifier defined in Rust. The recording service implements standard CRUD-style operations for the recorder:
// frontend/src/services/recordingService.ts
import { invoke } from '@tauri-apps/api/core';
export async function startRecording() {
return invoke('start_recording');
}
export async function stopRecording() {
return invoke('stop_recording');
}
export async function pauseRecording() {
return invoke('pause_recording');
}
export async function resumeRecording() {
return invoke('resume_recording');
}
Additional commands manage AI model lifecycles and analytics. The frontend invokes whisper_load_model to initialize transcription engines, builtin_ai_download_model for on-device LLMs, and track_event for telemetry, all following the same pattern of passing a command name and optional payload to invoke.
Listening for Rust Events in React
While commands follow a request-response pattern, events enable the Rust backend to push updates to the frontend asynchronously. The application imports ** listen ** from @tauri-apps/api/event to subscribe to channels that the Rust code emits via app.emit().
Recording Lifecycle Events
The recording service exposes functions that return ** UnlistenFn ** callbacks, allowing React components to subscribe to state changes:
// frontend/src/services/recordingService.ts
import { listen } from '@tauri-apps/api/event';
export async function onRecordingStarted(callback: () => void) {
return listen('recording-started', callback);
}
export async function onRecordingStopped(callback: () => void) {
return listen('recording-stopped', callback);
}
export async function onRecordingPaused(callback: () => void) {
return listen('recording-paused', callback);
}
export async function onRecordingResumed(callback: () => void) {
return listen('recording-resumed', callback);
}
Transcription Streaming Events
Real-time transcription updates flow through frontend/src/services/transcriptService.ts, which listens for chunk-level updates and completion signals:
// frontend/src/services/transcriptService.ts
import { listen } from '@tauri-apps/api/event';
export interface TranscriptPayload {
text: string;
isFinal: boolean;
}
export async function onTranscriptUpdate(callback: (event: { payload: TranscriptPayload }) => void) {
return listen('transcript-update', callback);
}
export async function onTranscriptionComplete(callback: (event: { payload: { text: string } }) => void) {
return listen('transcription-complete', callback);
}
export async function onTranscriptionError(callback: (event: { payload: { error: string } }) => void) {
return listen('transcription-error', callback);
}
Managing Listener Lifecycles in React Hooks
To prevent memory leaks, hooks like useRecordingStop (found in frontend/src/hooks/useRecordingStop.ts) store the unlisten functions returned by the service layer and invoke them during component unmount.
// frontend/src/hooks/useRecordingStop.ts
import { useEffect, useRef } from 'react';
import { onRecordingStopped } from '@/services/recordingService';
export function useRecordingStop(onStop: () => void) {
const unlistenRef = useRef<(() => void) | null>(null);
useEffect(() => {
const setupListener = async () => {
const unlisten = await onRecordingStopped(() => {
onStop();
});
unlistenRef.current = unlisten;
};
setupListener();
return () => {
if (unlistenRef.current) {
unlistenRef.current();
}
};
}, [onStop]);
}
This pattern ensures that Rust events do not attempt to update unmounted React components, which is critical in a desktop environment where users frequently switch views during long-running recordings.
Rust Implementation: Command Registration and Event Emission
On the Rust side, commands are defined in frontend/src-tauri/src/lib.rs using the #[tauri::command] attribute macro and registered via tauri::generate_handler!.
// frontend/src-tauri/src/lib.rs
#[tauri::command]
async fn start_recording(
state: State<'_, RecordingState>,
device_names: Vec<String>,
meeting_name: String,
) -> Result<String, String> {
// Audio pipeline initialization
Ok("recording_started".to_string())
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
start_recording,
stop_recording,
pause_recording,
resume_recording,
whisper_load_model,
track_event,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
During execution, Rust emits events through the Tauri application handle. For example, when recording actually begins, the backend emits the recording-started event that the frontend is already awaiting:
// frontend/src-tauri/src/audio/recordingManager.rs (conceptual)
app.emit("recording-started", Payload { timestamp: now });
Complete Data Flow Example
The full communication cycle illustrates the separation of concerns:
- User Action: React component calls
startRecording()service function - Invocation: Service executes
invoke('start_recording', { device_names, meeting_name }) - Rust Execution: Command handler in
lib.rsreceives arguments and initializes the audio pipeline - Event Emission: When hardware initialization succeeds, Rust calls
app.emit("recording-started", ...) - Frontend Update: The
onRecordingStartedlistener in the React hook receives the payload and updates component state - Cleanup: Component unmount triggers the
unlistenfunction, removing the subscription
This architecture ensures that computationally expensive work stays in Rust while the React UI remains responsive to user input.
Summary
- Command Invocation: The frontend uses
invokefrom@tauri-apps/api/coreinfrontend/src/services/recordingService.tsto trigger Rust functions likestart_recordingandwhisper_load_model. - Event Subscription: The
listenfunction from@tauri-apps/api/eventcreates subscriptions to Rust-emitted events includingrecording-started,transcript-update, andtranscription-complete. - Memory Management: React hooks store
UnlistenFncallbacks returned by listeners and clean them up on unmount to prevent leaks. - Rust Registration: Commands are defined with
#[tauri::command]infrontend/src-tauri/src/lib.rsand emitted viaapp.emit()from managers likerecordingManager.rs.
Frequently Asked Questions
How does Meetily pass arguments when invoking Tauri commands?
Meetily passes arguments as the second parameter to invoke. For example, when starting a recording, the frontend calls invoke('start_recording', { device_names, meeting_name }), where the object matches the parameter signature defined in the Rust #[tauri::command] handler. The Tauri framework automatically serializes the JavaScript object into Rust types.
What events does the Meetily Rust backend emit to the frontend?
The backend emits recording lifecycle events (recording-started, recording-stopped, recording-paused, recording-resumed) through recordingService.ts, and transcription-specific events (transcript-update, transcription-complete, transcription-error) through transcriptService.ts. These events carry typed payloads that TypeScript interfaces define for type safety.
How does Meetily prevent memory leaks from event listeners?
Each call to listen returns an UnlistenFn that removes the subscription. React hooks like useRecordingStop store this function in a ref and invoke it inside the effect cleanup function when the component unmounts, ensuring that Rust cannot emit events to destroyed React components.
Where are Tauri commands registered in the Meetily codebase?
Commands are registered in frontend/src-tauri/src/lib.rs using the tauri::generate_handler! macro, which takes a list of functions marked with #[tauri::command]. This registration occurs during the Tauri application build phase within the main() function before tauri::Builder::default().run() executes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →