C.W.K.
Stream
Lesson 05 of 06 · published

Streaming with Channel<T>

~13 min · tauri, channel, streaming, ipc

Level 0Web Tourist
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"An event is a public address system. A channel is a private phone line opened for one call."

When One Command Has Many Things to Say

A command returns once. But some work produces a stream of updates: a file download reporting bytes, a long job emitting progress, a generator yielding tokens. You could broadcast events, but then every listener everywhere hears them and you have to correlate which update belongs to which call. Tauri's Channel<T> solves this cleanly — it's a typed, ordered stream scoped to a single invoke.

How It Wires Up

The frontend creates a Channel, sets its onmessage handler, and passes it as a command argument. The Rust command receives a Channel<T> parameter and calls .send(payload) as often as it likes; each send fires the frontend handler in order. When the command returns, the stream for that call is done. No event-name collisions, no correlation IDs — the channel is the correlation.

Channel vs Event vs External Stream

Pick by scope. Event: global, anyone can listen, good for app-wide news. Channel: request-scoped, one caller, good for progress of this operation. And when the stream comes from a separate process entirely — not your Tauri core — neither applies; you reach for a real network transport. Cinder streams generated images from a separate brain process over a WebSocket, precisely because the producer isn't inside the Tauri core. Same 'streaming' word, three different right answers depending on where the data is born.

Code

A command that streams progress over a Channel·rust
use tauri::ipc::Channel;
use serde::Serialize;

#[derive(Clone, Serialize)]
struct Tick { step: u32, total: u32 }

// The command receives a Channel and streams into it.
#[tauri::command]
async fn run_job(on_event: Channel<Tick>) -> Result<(), String> {
    for step in 1..=10 {
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
        on_event.send(Tick { step, total: 10 }).map_err(|e| e.to_string())?;
    }
    Ok(()) // returning ends this call's stream
}
Receiving the stream on the frontend·tsx
import { invoke, Channel } from "@tauri-apps/api/core";

type Tick = { step: number; total: number };

const channel = new Channel<Tick>();
channel.onmessage = (msg) => {
  console.log(`step ${msg.step}/${msg.total}`);
};

// Pass the channel as an argument; updates arrive on onmessage in order.
await invoke("run_job", { onEvent: channel });

External links

Exercise

Write a command that streams 5 progress updates over a Channel with a short delay between each, and render a live progress bar on the frontend from channel.onmessage. Then articulate, in one sentence each, when you'd instead use an event and when you'd need a WebSocket — proving you can place all three on the scope spectrum.
Hint
Rust: on_event.send(...) in a loop with tokio sleep. Frontend: new Channel(), set onmessage, pass as { onEvent }. Event = global/app-wide; WebSocket = data from another process or a remote server.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.