prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use crate::engine::EngineState;
use crate::interfaces::{AttachedInterfaces, InterfaceId};
use crate::routing::routes::{NextHop, RouteEntry};
use crate::routing::warmth::WarmestOf;
use crate::storage::StorageLayout;
use crate::units::InstantMillis;
use crate::wire::DestinationHash;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnnounceRateState {
    pub destination: DestinationHash,
    pub last_allowed_announce_at: InstantMillis,
    pub blocked_until: InstantMillis,
    pub rate_violations: u16,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteSnapshot {
    pub destination: DestinationHash,
    pub hops: u8,
    pub via: NextHop,
    pub learned_at: InstantMillis,
    pub last_relayed_at: InstantMillis,
    pub expires_at: InstantMillis,
    pub interface: InterfaceId,
}

fn route_snapshot(
    destination: DestinationHash,
    entry: RouteEntry,
    expires_at: InstantMillis,
) -> RouteSnapshot {
    RouteSnapshot {
        destination,
        hops: entry.hops,
        via: entry.next_hop,
        learned_at: entry.learned_at,
        last_relayed_at: entry.last_relayed_at,
        expires_at,
        interface: entry.receiving_interface,
    }
}

impl<S: StorageLayout> EngineState<S> {
    #[must_use]
    pub fn link_count(&self) -> u32 {
        u32::try_from(self.links.active_link_count()).unwrap_or(u32::MAX)
    }

    pub fn visit_announce_rate_states(&self, mut visit: impl FnMut(AnnounceRateState)) {
        for (destination, entry) in self.destination_announce_limits.entries() {
            visit(AnnounceRateState {
                destination,
                last_allowed_announce_at: entry.last_allowed_announce_at,
                blocked_until: entry.blocked_until,
                rate_violations: entry.rate_violations,
            });
        }
    }

    pub fn visit_route_snapshots(
        &self,
        interfaces: AttachedInterfaces<'_>,
        mut visit: impl FnMut(RouteSnapshot),
    ) {
        let warmth = WarmestOf(&self.tunnels, &self.departed_interfaces);
        for (destination, entry, expires_at) in self
            .routing_table
            .path_rows_with_expiry(interfaces, &warmth)
        {
            visit(route_snapshot(destination, entry, expires_at));
        }
    }

    #[must_use]
    pub fn route_snapshot(
        &self,
        destination: DestinationHash,
        interfaces: AttachedInterfaces<'_>,
    ) -> Option<RouteSnapshot> {
        let warmth = WarmestOf(&self.tunnels, &self.departed_interfaces);
        self.routing_table
            .path_row_with_expiry(&destination, interfaces, &warmth)
            .map(|(entry, expires_at)| route_snapshot(destination, entry, expires_at))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::engine::test_support::TestStorageLayout;
    use crate::interfaces::AnnounceRateLimit;

    #[test]
    fn announce_rate_introspection_projects_engine_state() {
        let mut engine = EngineState::<TestStorageLayout>::default();
        let destination = DestinationHash::new([0x42; 16]);
        let limit = AnnounceRateLimit {
            target_ms: 100,
            grace: 3,
            penalty_ms: 1_000,
        };
        engine
            .destination_announce_limits
            .observe(destination, InstantMillis(10), limit);
        engine
            .destination_announce_limits
            .observe(destination, InstantMillis(20), limit);
        let mut inspected = None;

        engine.visit_announce_rate_states(|state| inspected = Some(state));

        assert_eq!(
            inspected,
            Some(AnnounceRateState {
                destination,
                last_allowed_announce_at: InstantMillis(20),
                blocked_until: InstantMillis(0),
                rate_violations: 1,
            })
        );
    }
}