El canal tipado
Cómo se monta: el frontend lo crea, lo pasa como argumento, y el core le manda mensajes. La forma que conviene darle a los mensajes, y por qué un enum etiquetado es la respuesta.
Los dos lados
use tauri::ipc::Channel;#[tauri::command]async fn importar(ruta: PathBuf, progreso: Channel<Progreso>) -> Result<(), ErrorPayload> { let total = contar(&ruta).await?; for (i, item) in leer(&ruta).await?.enumerate() { procesar(item).await?; progreso.send(Progreso::Avance { hechos: i + 1, total })?; } progreso.send(Progreso::Fin { total })?; Ok(())}import { Channel, invoke } from '@tauri-apps/api/core';const progreso = new Channel<Progreso>();progreso.onmessage = (m) => { if (m.tipo === 'avance') barra.set(m.hechos / m.total); if (m.tipo === 'fin') avisar(`Importados ${m.total}`);};await invoke('importar', { ruta, progreso }); // se pasa como un argumento másTres cosas de ese par:
- El canal es un argumento del comando, no algo inyectado. El framework lo reconoce por su tipo y lo conecta con el objeto que el frontend creó.
sendpuede fallar. Devuelve un resultado, y falla cuando ya no hay nadie al otro lado —porque la ventana se cerró, o la webview se recargó—. Ignorarlo con un descarte es aceptable para un progreso; para algo que importe, hay que mirarlo.- El comando sigue siendo un comando. Devuelve al final, y esa vuelta es la que dice si todo salió bien. El canal es el detalle, no el resultado.
La forma de los mensajes
La tentación es mandar un número —el porcentaje— y se queda corta enseguida. Un flujo real tiene más de un tipo de mensaje: avances, avisos, elementos que fallaron, el final.
La respuesta es un enum etiquetado, con las reglas del nivel 17.2:
#[derive(Clone, serde::Serialize, specta::Type)]#[serde(tag = "tipo", rename_all = "camelCase")]pub enum Progreso { Inicio { total: usize }, Avance { hechos: usize, total: usize }, Aviso { indice: usize, motivo: String }, Fin { total: usize, fallidos: usize },}Y al otro lado, una unión discriminada que el compilador estrecha:
progreso.onmessage = (m) => { switch (m.tipo) { case 'inicio': barra.mostrar(m.total); break; case 'avance': barra.set(m.hechos / m.total); break; case 'aviso': lista.añadir(m.indice, m.motivo); break; case 'fin': barra.ocultar(); resumen(m.total, m.fallidos); break; }};Manda siempre el total en cada avance, aunque parezca redundante. Un frontend que se suscribe tarde, o que perdió el mensaje inicial, puede pintar la barra igualmente. Es un campo por mensaje a cambio de no depender del orden.
Qué pasa al terminar
El canal vive lo que dura la llamada. Cuando el comando devuelve, el canal se cierra: mandar por él después no llega a ninguna parte.
De ahí una consecuencia práctica: si arrancas una tarea de fondo dentro del comando y devuelves enseguida —el patrón del nivel 16.3— el canal ya no sirve, porque el comando terminó. Para eso hacen falta eventos, que es exactamente el reparto del nivel 21.1: el canal es para el flujo que pertenece a la llamada, y una llamada que vuelve enseguida no tiene flujo propio.
El patrón correcto con canal es el contrario: el comando no vuelve hasta que la operación acaba, y va informando por el canal mientras tanto. Como es asíncrono —nivel 15.1— no bloquea nada, y la promesa del frontend se resuelve cuando de verdad ha terminado, que es información útil.