use serde::{Deserialize, Serialize};
use std::time::Instant;
pub type PeerId = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PeerRole {
Root,
Relay,
Leaf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerInfo {
pub id: PeerId,
pub track: String,
pub role: PeerRole,
pub parent: Option<PeerId>,
pub children: Vec<PeerId>,
pub depth: u32,
pub upload_kbps: u32,
#[serde(default)]
pub forwarded_frames: u64,
#[serde(default)]
pub capacity: Option<u32>,
#[serde(skip)]
pub joined_at: Option<Instant>,
#[serde(skip)]
pub last_heartbeat: Option<Instant>,
}
impl PeerInfo {
pub fn new(id: PeerId, track: String) -> Self {
Self {
id,
track,
role: PeerRole::Leaf,
parent: None,
children: Vec::new(),
depth: 0,
upload_kbps: 5000, forwarded_frames: 0,
capacity: None,
joined_at: Some(Instant::now()),
last_heartbeat: Some(Instant::now()),
}
}
pub fn can_accept_child(&self, default_max: usize) -> bool {
self.children.len() < self.effective_capacity(default_max)
}
pub fn effective_capacity(&self, default_max: usize) -> usize {
self.capacity
.map(|c| (c as usize).min(default_max))
.unwrap_or(default_max)
}
pub fn child_count(&self) -> usize {
self.children.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerAssignment {
pub peer_id: PeerId,
pub role: PeerRole,
pub parent: Option<PeerId>,
pub depth: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn peer_info_defaults() {
let peer = PeerInfo::new("peer-1".into(), "live/test".into());
assert_eq!(peer.id, "peer-1");
assert_eq!(peer.role, PeerRole::Leaf);
assert!(peer.parent.is_none());
assert!(peer.children.is_empty());
assert_eq!(peer.depth, 0);
assert_eq!(peer.upload_kbps, 5000);
assert_eq!(peer.forwarded_frames, 0);
assert!(peer.capacity.is_none());
}
#[test]
fn can_accept_child() {
let mut peer = PeerInfo::new("peer-1".into(), "live/test".into());
assert!(peer.can_accept_child(3));
peer.children.push("child-1".into());
peer.children.push("child-2".into());
peer.children.push("child-3".into());
assert!(!peer.can_accept_child(3));
}
#[test]
fn effective_capacity_uses_global_when_unset() {
let peer = PeerInfo::new("peer-1".into(), "live/test".into());
assert_eq!(peer.effective_capacity(3), 3);
assert_eq!(peer.effective_capacity(0), 0);
}
#[test]
fn effective_capacity_clamps_oversize_claim() {
let mut peer = PeerInfo::new("peer-1".into(), "live/test".into());
peer.capacity = Some(u32::MAX);
assert_eq!(peer.effective_capacity(5), 5);
}
#[test]
fn effective_capacity_uses_smaller_of_claim_and_default() {
let mut peer = PeerInfo::new("peer-1".into(), "live/test".into());
peer.capacity = Some(2);
assert_eq!(peer.effective_capacity(5), 2);
assert_eq!(peer.effective_capacity(1), 1);
}
#[test]
fn can_accept_child_respects_capacity_zero() {
let mut peer = PeerInfo::new("peer-1".into(), "live/test".into());
peer.capacity = Some(0);
assert!(
!peer.can_accept_child(10),
"capacity=0 must reject children even with default_max=10"
);
}
#[test]
fn can_accept_child_respects_per_peer_cap_below_default() {
let mut peer = PeerInfo::new("peer-1".into(), "live/test".into());
peer.capacity = Some(1);
assert!(peer.can_accept_child(5));
peer.children.push("child-1".into());
assert!(
!peer.can_accept_child(5),
"peer with capacity=1 must refuse a second child even when default_max=5"
);
}
}