//! Minimal FAPP ([`quicprochat_p2p::fapp`]) demo: therapist publishes a slot, relay caches it, //! patient floods a query; the relay answers from its in-memory store. //! //! Uses mock [`TransportAddr`] values and [`RoutingTable`](quicprochat_p2p::routing_table::RoutingTable) //! seeds — no sockets or iroh. //! //! Run: `cargo run -p quicprochat-p2p --example fapp_demo` use std::sync::{Arc, RwLock}; use std::time::Duration; use anyhow::{Context, Result}; use quicprochat_p2p::announce::{MeshAnnounce, CAP_RELAY}; use quicprochat_p2p::fapp::{ Fachrichtung, FappStore, Kostentraeger, Modalitaet, SlotAnnounce, SlotQuery, SlotType, TimeSlot, CAP_FAPP_PATIENT, CAP_FAPP_RELAY, CAP_FAPP_THERAPIST, }; use quicprochat_p2p::fapp_router::{FappAction, FappRouter}; use quicprochat_p2p::identity::MeshIdentity; use quicprochat_p2p::routing_table::RoutingTable; use quicprochat_p2p::transport::TransportAddr; use quicprochat_p2p::transport_manager::TransportManager; /// Insert one synthetic route so [`FappRouter::broadcast_announce`] / [`FappRouter::send_query`] /// have a next hop (see [`quicprochat_p2p::fapp_router::FappRouter::drain_pending_sends`]). fn seed_mock_neighbor(table: &mut RoutingTable, next_hop: TransportAddr) { let peer = MeshIdentity::generate(); let mut announce = MeshAnnounce::with_sequence(&peer, CAP_RELAY, vec![], 8, 1); announce.hop_count = 1; let _ = table.update(&announce, "mock", next_hop); } fn main() -> Result<()> { let relay_inbox: TransportAddr = TransportAddr::Raw(b"link-therapist-to-relay".to_vec()); let patient_outbound: TransportAddr = TransportAddr::Raw(b"link-patient-flood".to_vec()); // --- Therapist: can publish; routing table points at the "relay" hop --- let therapist_routes = Arc::new(RwLock::new(RoutingTable::new(Duration::from_secs(300)))); { let mut table = therapist_routes .write() .map_err(|e| anyhow::anyhow!("therapist routes lock poisoned: {e}"))?; seed_mock_neighbor(&mut table, relay_inbox.clone()); } let therapist = FappRouter::new( FappStore::new(), therapist_routes, Arc::new(TransportManager::new()), CAP_FAPP_THERAPIST, ); // --- Patient + relay: start with an empty table so the announce is cached but not re-flooded --- let patient_relay_routes = Arc::new(RwLock::new(RoutingTable::new(Duration::from_secs(300)))); let patient_relay = FappRouter::new( FappStore::new(), Arc::clone(&patient_relay_routes), Arc::new(TransportManager::new()), CAP_FAPP_PATIENT | CAP_FAPP_RELAY, ); let therapist_id = MeshIdentity::generate(); let announce = SlotAnnounce::new( &therapist_id, vec![Fachrichtung::Verhaltenstherapie], vec![Modalitaet::Praxis], vec![Kostentraeger::GKV], "80331".into(), vec![TimeSlot { start_unix: 1, duration_minutes: 50, slot_type: SlotType::Therapie, }], [0xAA; 32], 1, ); // 1) Therapist broadcasts SlotAnnounce (queued to mock relay address). therapist .broadcast_announce(announce.clone()) .context("broadcast_announce")?; let mut pending = therapist .drain_pending_sends() .context("therapist drain_pending_sends")?; let (to_relay, announce_wire) = pending .pop() .context("expected one pending frame from therapist")?; println!("Therapist queued announce -> {to_relay} ({} bytes)", announce_wire.len()); assert_eq!(to_relay, relay_inbox); // 2) Relay receives the wire frame (in real code: [`TransportManager::send`] / recv). let relay_action = patient_relay.handle_incoming(&announce_wire); println!("Relay handled announce: {relay_action:?}"); // Add a mock neighbor so `send_query` can enqueue a flood (API demo only). { let mut table = patient_relay_routes .write() .map_err(|e| anyhow::anyhow!("patient/relay routes lock poisoned: {e}"))?; seed_mock_neighbor(&mut table, patient_outbound.clone()); } // 3) Patient floods a SlotQuery. let query = SlotQuery { query_id: [0x42; 16], fachrichtung: Some(Fachrichtung::Verhaltenstherapie), modalitaet: None, kostentraeger: None, plz_prefix: Some("803".into()), earliest: None, latest: None, slot_type: None, max_results: 5, }; patient_relay.send_query(query).context("send_query")?; pending = patient_relay .drain_pending_sends() .context("patient drain_pending_sends")?; let (flood_dest, query_wire) = pending .pop() .context("expected one pending query frame")?; println!("Patient queued query flood -> {flood_dest} ({} bytes)", query_wire.len()); assert_eq!(flood_dest, patient_outbound); // 4) Same relay node decodes the query and answers from [`FappStore`]. match patient_relay.handle_incoming(&query_wire) { FappAction::QueryResponse(resp) => { println!( "Relay query response: query_id={:02x?}.. matches={}", &resp.query_id[..4], resp.matches.len() ); let first = resp .matches .first() .context("expected at least one matching announce")?; assert_eq!(first.id, announce.id); } other => anyhow::bail!("expected QueryResponse, got {other:?}"), } Ok(()) }