examples/fapp_demo.rs: - Therapist publishes SlotAnnounce - Relay caches and handles query - Patient sends SlotQuery, gets response - Shows full FappRouter API flow docs/status.md: - Updated FAPP integration status - FappRouter now implemented - Remaining: multi-node test, SlotReserve/Confirm, LoRa
143 lines
5.4 KiB
Rust
143 lines
5.4 KiB
Rust
//! 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(())
|
|
}
|