Files
tether/core/examples/iroh_engine.rs
Patrick Ecord 3cb916ee5e core: M4 — files/photos over iroh-blobs (multi-modal clipboard)
IrohTransport.send_file now adds the (already-sealed) bytes to a MemStore and
broadcasts a blob announcement over gossip {hash, name, mime, provider}; on
receipt, peers fetch the blob P2P by hash via iroh-blobs and emit MeshEvent::File.
The engine's existing File handler decrypts and delivers — so the clipboard can
carry data/photos/etc, not just text. Replaces the hand-rolled M/C/E frame
protocol entirely.

Verified: `cargo run --features iroh-transport --example iroh_engine` syncs both
a text clipboard (gossip) AND a 200 KB photo (iroh-blobs), both E2E-decrypted
A→B, no SSE/RTC/LAN/relay. Default ship build untouched; feature build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 01:15:41 -05:00

118 lines
4.2 KiB
Rust

//! M4 integration test — the real `Engine` syncing a clipboard over iroh.
//! cargo run --features iroh-transport --example iroh_engine
//!
//! Two engines, same account → same room → same gossip topic. No SSE/RTC/LAN:
//! with the iroh-transport feature the engine's only transport is IrohTransport.
//! Proves the public API (Engine::new/start/send + MessageHandler) is unchanged
//! while the entire Mesh underneath is iroh — the payoff of the MeshEvent seam.
//!
//! Bootstrap is wired in-process via the TETHER_IROH_BOOTSTRAP env var (the v1
//! stopgap until the rendezvous supernode): start A, read its EndpointId, hand
//! it to B.
use std::sync::mpsc;
use std::time::{Duration, Instant};
use tethercore::{Engine, Message, MessageHandler};
struct Noop;
impl MessageHandler for Noop {
fn on_message(&self, _m: Message) {}
fn on_status(&self, _c: bool) {}
fn on_file(&self, _n: String, _m: String, _d: Vec<u8>) {}
}
struct Collector {
text: mpsc::Sender<String>,
file: mpsc::Sender<(String, usize)>,
}
impl MessageHandler for Collector {
fn on_message(&self, m: Message) {
let _ = self.text.send(m.text);
}
fn on_status(&self, _c: bool) {}
fn on_file(&self, name: String, _mime: String, data: Vec<u8>) {
let _ = self.file.send((name, data.len()));
}
}
fn main() {
let account = "pecord@gmail.com"; // same account → same room → same topic + key
let room = "iroh-engine-test";
let server = "unused"; // iroh ignores cfg.server; it uses n0 infra
// Opener (no bootstrap yet).
let a = Engine::new(
server.into(), room.into(), "iroh-a".into(), "macos".into(), "A".into(), account.into(),
);
a.start(Box::new(Noop));
// Wait for A's iroh endpoint to come online, then bootstrap B off it.
let a_id = {
let deadline = Instant::now() + Duration::from_secs(20);
loop {
if let Some(id) = a.iroh_id() {
break id;
}
if Instant::now() > deadline {
eprintln!("❌ A never came online");
std::process::exit(1);
}
std::thread::sleep(Duration::from_millis(200));
}
};
eprintln!("A online, id={a_id}");
std::env::set_var("TETHER_IROH_BOOTSTRAP", &a_id);
// Joiner (bootstraps to A via the env var its IrohTransport reads at start).
let (tx, rx) = mpsc::channel();
let (ftx, frx) = mpsc::channel();
let b = Engine::new(
server.into(), room.into(), "iroh-b".into(), "ios".into(), "B".into(), account.into(),
);
b.start(Box::new(Collector { text: tx, file: ftx }));
eprintln!("waiting for gossip topic to form…");
std::thread::sleep(Duration::from_secs(8));
// 1) Text clipboard over gossip.
let payload = "clipboard synced over iroh 🎉";
eprintln!("A sending text: {payload:?}");
a.send(payload.to_string());
match rx.recv_timeout(Duration::from_secs(10)) {
Ok(t) if t == payload => {
println!("✅ text: B received {t:?} (E2E-decrypted, via gossip)");
}
Ok(t) => {
println!("⚠️ B got {t:?} (expected {payload:?})");
std::process::exit(1);
}
Err(_) => {
println!("❌ B received no text");
std::process::exit(1);
}
}
// 2) A "photo"/blob over iroh-blobs (announced via gossip, fetched P2P).
let photo: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
eprintln!("A sending blob: photo.bin ({} bytes)", photo.len());
a.send_file("photo.bin".into(), "image/png".into(), photo.clone());
match frx.recv_timeout(Duration::from_secs(15)) {
Ok((name, len)) if len == photo.len() => {
println!("✅ blob: B received {name:?} ({len} bytes, E2E-decrypted, via iroh-blobs)");
}
Ok((name, len)) => {
println!("⚠️ B got {name:?} with {len} bytes (expected {})", photo.len());
std::process::exit(1);
}
Err(_) => {
println!("❌ B received no blob");
std::process::exit(1);
}
}
println!("🎉 engine-over-iroh: text + photo synced, no SSE/RTC/LAN/relay");
a.stop();
b.stop();
}