Tenho seguinte código rust javascript

rust

use tauri::{AppHandle, ipc::Channel};
use serde::Serialize;

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
enum DownloadEvent<'a> {
  #[serde(rename_all = "camelCase")]
  Started {
    url: &'a str,
    download_id: usize,
    content_length: usize,
  },
  #[serde(rename_all = "camelCase")]
  Progress {
    download_id: usize,
    chunk_length: usize,
  },
  #[serde(rename_all = "camelCase")]
  Finished {
    download_id: usize,
  },
}

#[tauri::command]
fn download(app: AppHandle, url: String, on_event: Channel<DownloadEvent>) {
  let content_length = 1000;
  let download_id = 1;

  on_event.send(DownloadEvent::Started {
    url: &url,
    download_id,
    content_length,
  }).unwrap();

  for chunk_length in [15, 150, 35, 500, 300] {
    on_event.send(DownloadEvent::Progress {
      download_id,
      chunk_length,
    }).unwrap();
  }

  on_event.send(DownloadEvent::Finished { download_id }).unwrap();
}

javascript

import { invoke, Channel } from '@tauri-apps/api/core';

type DownloadEvent =
  | {
      event: 'started';
      data: {
        url: string;
        downloadId: number;
        contentLength: number;
      };
    }
  | {
      event: 'progress';
      data: {
        downloadId: number;
        chunkLength: number;
      };
    }
  | {
      event: 'finished';
      data: {
        downloadId: number;
      };
    };

const onEvent = new Channel<DownloadEvent>();
onEvent.onmessage = (message) => {
  console.log(`got download event ${message.event}`);
};

await invoke('download', {
  url: 'https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-schema-generator/schemas/config.schema.json',
  onEvent,
});

Agora eu tenho um código que gostaria de aplicar o Event em vez de emit

use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

use tauri::{AppHandle, Emitter};
use reqwest::Client;
use tokio::fs as tokio_fs;
use tokio::io::AsyncWriteExt;
use std::time::Instant;

#[derive(Debug, Deserialize)]
struct PatchEntry {
    file_name: String,
}

#[derive(Debug, Serialize, Clone)]
struct DownloadMessage {
    message: String,
}

#[derive(Debug, Serialize, Clone)]
struct DownloadInfo {
    current_file: usize,
    total_files: usize,
    file_name: String,
    downloaded_bytes: u64,
    total_bytes: u64,
}

fn get_exe_dir() -> PathBuf {
    std::env::current_exe()
        .unwrap()
        .parent()
        .unwrap()
        .to_path_buf()
}

// Função de leitura de patches assíncrona
async fn read_patch_file(path: &Path) -> Result<Vec<PatchEntry>, Box<dyn std::error::Error>> {
    let content = tokio_fs::read_to_string(path).await?;
    let patches: Vec<PatchEntry> = serde_json::from_str(&content)?;
    Ok(patches)
}

async fn download_file(
    app_handle: &AppHandle,
    client: &Client,
    entry: PatchEntry,
    download_dir: &Path,
    current_file: usize,
    total_files: usize,
) -> Result<(), Box<dyn std::error::Error>> {
    let url = format!("https://hil-speed.hetzner.com/{}", entry.file_name);
    let file_path = download_dir.join(&entry.file_name);

    let mut response = client.get(&url).send().await?;
    let total_bytes = response.content_length().unwrap_or(0);
    let mut file = tokio_fs::OpenOptions::new()
        .write(true)
        .create(true)
        .open(file_path)
        .await?;

    let mut downloaded_bytes = 0;
    let start_time = Instant::now();

    while let Some(chunk) = response.chunk().await? {
        file.write_all(&chunk).await?;
        downloaded_bytes += chunk.len() as u64;

        let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64();
        let speed = if elapsed_time > 0.0 {
            downloaded_bytes as f64 / elapsed_time
        } else {
            0.0
        };

        app_handle.emit("download_message", DownloadMessage { message: "Baixando".to_string() })?;
        app_handle.emit("download_info", DownloadInfo {
            current_file,
            total_files,
            file_name: entry.file_name.clone(),
            downloaded_bytes,
            total_bytes,
        })?;
        app_handle.emit("download_speed", speed)?;
    }

    app_handle.emit("download_message", DownloadMessage { message: "Download concluído".to_string() })?;
    Ok(())
}

async fn check_server_connection(client: &Client, url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let response = client.get(url).send().await?;
    if response.status().is_success() {
        Ok(())
    } else {
        Err("Failed to connect to the server".into())
    }
}

#[tauri::command]
pub async fn start_downloads(app_handle: AppHandle) -> Result<(), String> {
    let exe_dir = get_exe_dir();
    let patch_file_path = exe_dir.join("patch.json");
    let download_dir = exe_dir.join("patchs");

    if !download_dir.exists() {
        tokio_fs::create_dir_all(&download_dir).await.map_err(|e| e.to_string())?;
    }

    let patches = read_patch_file(&patch_file_path).await.map_err(|e| e.to_string())?;
    let client = Client::new();
    let total_files = patches.len();

    app_handle.emit("download_message", DownloadMessage {
        message: "Conectando ao servidor...".to_string(),
    }).map_err(|e| e.to_string())?;

    let server_url = "https://hil-speed.hetzner.com/";
    if let Err(err) = check_server_connection(&client, server_url).await {
        app_handle.emit("download_message", DownloadMessage {
            message: format!("Erro ao conectar ao servidor: {}", err),
        }).map_err(|e| e.to_string())?;
        return Err(format!("Erro ao conectar ao servidor: {}", err));
    }

    app_handle.emit("download_message", DownloadMessage {
        message: "Conexão estabelecida com sucesso. Iniciando download...".to_string(),
    }).map_err(|e| e.to_string())?;

    // Executando downloads sequencialmente
    for (current_file, entry) in patches.into_iter().enumerate() {
        let current_file = current_file + 1;
        if let Err(err) = download_file(&app_handle, &client, entry, &download_dir, current_file, total_files).await {
            eprintln!("Erro ao baixar arquivo: {}", err);
            app_handle.emit("download_message", DownloadMessage {
                message: format!("Erro ao baixar arquivo: {}", err),
            }).map_err(|e| e.to_string())?;
        }
    }

    Ok(())
}

e aplicar também no meu react

import { useEffect, useState } from "react";
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";

function formatBytes(bytes: number) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  const value = bytes / Math.pow(k, i);
  return `${value.toFixed(2)} ${units[i]}`;
}

interface DownloadMessage {
  message: string;
}

interface DownloadInfo {
  current_file: number;
  total_files: number;
  file_name: string;
  downloaded_bytes: number;
  total_bytes: number;
}


const DownloadManager = () => {
  const [message, setMessage] = useState("");
  const [currentFile, setCurrentFile] = useState(0);
  const [totalFiles, setTotalFiles] = useState(0);
  const [fileName, setFileName] = useState("");
  const [downloadedBytes, setDownloadedBytes] = useState(0);
  const [totalBytes, setTotalBytes] = useState(0);
  const [isDownloading, setIsDownloading] = useState(false);
  const [downloadSpeed, setDownloadSpeed] = useState(0);

  useEffect(() => {
    const unlistenUpdate = listen<DownloadMessage>("download_message", (event) => {
      const { message } = event.payload;
      setMessage(message);
    });

    const unlistenInfo = listen<DownloadInfo>("download_info", (event) => {
      const {
        current_file,
        total_files,
        file_name,
        downloaded_bytes,
        total_bytes,
      } = event.payload;

      setCurrentFile(current_file);
      setTotalFiles(total_files);
      setFileName(file_name);
      setDownloadedBytes(downloaded_bytes);
      setTotalBytes(total_bytes);
    });

    const unlistenSpeed = listen<number>("download_speed", (event) => {
      const speedInBytes = event.payload;
      setDownloadSpeed(speedInBytes);
    });

    return () => {
      unlistenUpdate.then((fn) => fn());
      unlistenInfo.then((fn) => fn());
      unlistenSpeed.then((fn) => fn());
    };
  }, [isDownloading]);

  const startDownload = async () => {
    if (isDownloading) return;
    setIsDownloading(true);

    try {
      await invoke("start_downloads");
    } catch (error) {
      setMessage(`Erro ao iniciar o download: ${error}`);
    } finally {
      setIsDownloading(false);
    }
  };

  const progressPercent =
    totalBytes > 0 ? (downloadedBytes / totalBytes) * 100 : 0;

  return (
    <>
      <div className="absolute bottom-20 rounded-lg w-[71%] h-[8%] flex flex-col justify-between items-center">
        <button onClick={startDownload} className="bg-blue-500 text-white rounded p-2 mb-2">
          Iniciar Download
        </button>
      </div>
      <div className="absolute bottom-3 rounded-lg w-[71%] h-[8%] flex flex-col justify-between items-center">
        <div className="w-full flex items-center">
          <p className="text-[14px] font-bold text-opacity-[0.95] text-white flex-grow text-center">
            {message}
            {isDownloading && totalFiles > 0 && (
              <span className="mx-1">( {currentFile} / {totalFiles} )</span>
            )}
            {isDownloading && fileName && (
              <>
                : <span className="text-orange-500">{fileName}</span>
                <span className="mx-1">( <span className="text-green-600">{formatBytes(downloadedBytes)}</span> / {formatBytes(totalBytes)} )</span>
              </>
            )}
          </p>
        </div>
        {isDownloading && (
          <div className="w-[30%] absolute h-6 flex justify-end items-center right-6 top-2">
            <span className="text-[10px] font-bold text-opacity-[0.95] text-yellow-600 text-right">
              {formatBytes(downloadSpeed)} /s
            </span>
          </div>
        )}
        <div className="w-[95%] bg-[#0E1622]/[0.95] text-green-600 rounded-md h-6 flex items-center shadow-inner shadow-gray-800">
          <div
            className="bg-[#1164CA]/[0.95] ml-1 rounded-md h-4"
            style={{ width: `${progressPercent}%` }}
          />
        </div>
      </div>
    </>
  );
};

export default DownloadManager;