feat(fapp): add integration demo + update status
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
This commit is contained in:
142
examples/fapp_demo.rs
Normal file
142
examples/fapp_demo.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
//! 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user