← All Workshops

MudEngine Part 11: Peer-to-Peer (Hybrid)

Step 8 / 12

Spawn iroh from the App component

The host needs to start the iroh endpoint, gossip, and (optionally) the relay. Desktop clients need to connect to the host's endpoint.

Use use_effect to spawn async tasks when the component mounts. The signals (players, rooms, chat_messages) are written from the iroh message loop, just like they were from the WebSocket receive loop before.

mud-engine/src/main.rs
// ── Inside App component, after the connection screen ──
if *mode.read() == "host" {
    let name = player_name.read().clone();
    use_effect(move || {
        tokio::spawn(async move {
            let engine = Arc::new(GameEngine::load("game.toml"));
            let (endpoint, sink, _handle) = p2p::start_host(engine.clone(), true).await?;

            println!("🔗 Share this EndpointId with players:");
            println!("   {}", endpoint.node_id());

            // If relay feature is enabled, spawn the WebSocket bridge
            #[cfg(feature = "relay")]
            if let Err(e) = p2p::spawn_relay(
                sink.clone(),
                broadcast_receiver,
                9090,
            ).await {
                eprintln!("Relay error: {e}");
            }

            Ok::<_, anyhow::Error>(())
        });
    });
}
mud-engine/src/main.rs
// ── Inside App component, when joining ──
if *mode.read() == "join" {
    let host_id = host_id_input.read().clone();
    use_effect(move || {
        tokio::spawn(async move {
            let (_endpoint, sink, mut rx) = p2p::connect_to_host(&host_id).await?;

            // Join the game
            p2p::broadcast(&sink, &ServerMessage::State {
                players: vec![],
                your_id: String::new(),
                rooms: vec![],
                quest_active: false,
                quest_completed: false,
            }).await;

            // Forward server messages to Dioxus signals
            while let Some(msg) = rx.recv().await {
                // Write to signals — same pattern as Part 7-9 receive loop
            }

            Ok::<_, anyhow::Error>(())
        });
    });
}
💡 DioxusNativePreset vs tokio

The Blitz renderer uses tokio internally. When you spawn a tokio task from use_effect, it runs on the same runtime that drives the UI. This means iroh's async operations and the Dioxus render loop coexist in the same threadpool — no special wiring needed.

If you see tokio runtime panics, make sure the iroh tasks are spawned from within a Dioxus component (which guarantees a tokio context). Never call tokio::spawn before dioxus_native::launch.

Step 8 / 12