use std::sync::LazyLock;
use dashmap::DashMap;
use freenet_stdlib::prelude::DelegateKey;
use tokio::sync::mpsc;
use crate::client_events::{ClientId, HostResult};
pub(crate) const MAX_APPS_PER_DELEGATE: usize = 128;
pub(crate) const MAX_DELEGATES_PER_CLIENT: usize = 256;
pub(crate) const REGISTRATION_TTL: std::time::Duration = std::time::Duration::from_secs(30 * 60);
struct AppRegistration {
client_id: ClientId,
sender: mpsc::Sender<HostResult>,
last_seen: tokio::time::Instant,
}
static DELEGATE_APPS: LazyLock<DashMap<DelegateKey, Vec<AppRegistration>>> =
LazyLock::new(DashMap::default);
static CLIENT_REGISTRATION_COUNTS: LazyLock<DashMap<ClientId, usize>> =
LazyLock::new(DashMap::default);
pub(crate) fn register_app(
delegate_key: &DelegateKey,
client_id: ClientId,
sender: mpsc::Sender<HostResult>,
) -> bool {
let now = tokio::time::Instant::now();
let mut apps = DELEGATE_APPS.entry(delegate_key.clone()).or_default();
if let Some(existing) = apps.iter_mut().find(|a| a.client_id == client_id) {
existing.sender = sender;
existing.last_seen = now;
return true;
}
if apps.len() >= MAX_APPS_PER_DELEGATE {
tracing::warn!(
delegate = %delegate_key,
%client_id,
cap = MAX_APPS_PER_DELEGATE,
"Rejecting app registration: delegate at max apps"
);
return false;
}
let mut count = CLIENT_REGISTRATION_COUNTS.entry(client_id).or_insert(0);
if *count >= MAX_DELEGATES_PER_CLIENT {
tracing::warn!(
delegate = %delegate_key,
%client_id,
cap = MAX_DELEGATES_PER_CLIENT,
"Rejecting app registration: client at max delegate registrations"
);
return false;
}
apps.push(AppRegistration {
client_id,
sender,
last_seen: now,
});
*count += 1;
true
}
pub(crate) fn route_to_apps(delegate_key: &DelegateKey, message: HostResult) -> usize {
let Some(mut apps) = DELEGATE_APPS.get_mut(delegate_key) else {
return 0;
};
let mut delivered = 0usize;
let mut closed_clients: Vec<ClientId> = Vec::new();
apps.retain(|app| match app.sender.try_send(message.clone()) {
Ok(()) => {
delivered += 1;
true
}
Err(mpsc::error::TrySendError::Full(_)) => {
tracing::warn!(
delegate = %delegate_key,
client_id = %app.client_id,
"App notification channel full — delegate ApplicationMessage dropped"
);
true
}
Err(mpsc::error::TrySendError::Closed(_)) => {
closed_clients.push(app.client_id);
false
}
});
let now_empty = apps.is_empty();
drop(apps);
for client_id in closed_clients {
decrement_client_count(client_id);
}
if now_empty {
DELEGATE_APPS.remove_if(delegate_key, |_, v| v.is_empty());
}
delivered
}
pub(crate) fn remove_client(client_id: ClientId) {
let mut removed_any = false;
DELEGATE_APPS.retain(|_, apps| {
let before = apps.len();
apps.retain(|a| a.client_id != client_id);
removed_any |= apps.len() != before;
!apps.is_empty()
});
if removed_any {
CLIENT_REGISTRATION_COUNTS.remove(&client_id);
}
}
pub(crate) fn remove_delegate(delegate_key: &DelegateKey) {
if let Some((_, apps)) = DELEGATE_APPS.remove(delegate_key) {
for app in apps {
decrement_client_count(app.client_id);
}
}
}
pub(crate) fn sweep_expired() {
let now = tokio::time::Instant::now();
let mut expired: Vec<ClientId> = Vec::new();
DELEGATE_APPS.retain(|_, apps| {
apps.retain(|a| {
let keep = now.saturating_duration_since(a.last_seen) < REGISTRATION_TTL;
if !keep {
expired.push(a.client_id);
}
keep
});
!apps.is_empty()
});
for client_id in expired {
decrement_client_count(client_id);
}
}
fn decrement_client_count(client_id: ClientId) {
if let Some(mut count) = CLIENT_REGISTRATION_COUNTS.get_mut(&client_id) {
*count = count.saturating_sub(1);
if *count == 0 {
drop(count);
CLIENT_REGISTRATION_COUNTS.remove_if(&client_id, |_, v| *v == 0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use freenet_stdlib::prelude::CodeHash;
use std::sync::atomic::{AtomicUsize, Ordering};
static NS: AtomicUsize = AtomicUsize::new(1);
struct Namespace(usize);
impl Namespace {
fn new() -> Self {
Namespace(NS.fetch_add(1, Ordering::Relaxed))
}
fn key(&self, n: usize) -> DelegateKey {
let mut bytes = [0u8; 32];
bytes[0..8].copy_from_slice(&(self.0 as u64).to_le_bytes());
bytes[8..16].copy_from_slice(&(n as u64).to_le_bytes());
DelegateKey::new(bytes, CodeHash::new(bytes))
}
fn client(&self, n: usize) -> ClientId {
ClientId((self.0 << 24) | (n & 0xFF_FFFF))
}
}
fn host_msg() -> HostResult {
use freenet_stdlib::client_api::HostResponse;
use freenet_stdlib::prelude::{ApplicationMessage, OutboundDelegateMsg};
Ok(HostResponse::DelegateResponse {
key: DelegateKey::new([0u8; 32], CodeHash::new([0u8; 32])),
values: vec![OutboundDelegateMsg::ApplicationMessage(
ApplicationMessage::new(vec![1]),
)],
})
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn register_and_route_delivers_to_app() {
let ns = Namespace::new();
let dk = ns.key(0);
let (tx, mut rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(&dk, ns.client(0), tx));
let delivered = route_to_apps(&dk, host_msg());
assert_eq!(delivered, 1);
assert!(rx.try_recv().is_ok(), "app must receive the message");
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn route_to_unknown_delegate_delivers_nothing() {
let ns = Namespace::new();
let delivered = route_to_apps(&ns.key(0), host_msg());
assert_eq!(delivered, 0);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn per_delegate_cap_rejects_excess_apps() {
let ns = Namespace::new();
let dk = ns.key(0);
for i in 0..MAX_APPS_PER_DELEGATE {
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&dk, ns.client(i), tx));
}
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(
!register_app(&dk, ns.client(MAX_APPS_PER_DELEGATE), tx),
"registration past per-delegate cap must be rejected"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn per_client_cap_rejects_excess_delegates() {
let ns = Namespace::new();
let client = ns.client(0);
for i in 0..MAX_DELEGATES_PER_CLIENT {
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&ns.key(i), client, tx));
}
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(
!register_app(&ns.key(MAX_DELEGATES_PER_CLIENT), client, tx),
"registration past per-client cap must be rejected"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn remove_client_frees_registrations() {
let ns = Namespace::new();
let dk = ns.key(0);
let client = ns.client(0);
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&dk, client, tx));
remove_client(client);
assert_eq!(route_to_apps(&dk, host_msg()), 0);
for i in 0..MAX_DELEGATES_PER_CLIENT {
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&ns.key(i + 1), client, tx));
}
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn closed_channel_is_pruned_on_route() {
let ns = Namespace::new();
let dk = ns.key(0);
let client = ns.client(0);
let (tx, rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&dk, client, tx));
drop(rx); assert_eq!(route_to_apps(&dk, host_msg()), 0);
for i in 0..MAX_DELEGATES_PER_CLIENT {
let (t, _r) = mpsc::channel::<HostResult>(1);
assert!(register_app(&ns.key(i + 1), client, t));
}
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn ttl_sweep_evicts_stale_registration() {
let ns = Namespace::new();
let dk = ns.key(0);
let (tx, _rx) = mpsc::channel::<HostResult>(1);
assert!(register_app(&dk, ns.client(0), tx));
tokio::time::advance(REGISTRATION_TTL + std::time::Duration::from_secs(1)).await;
sweep_expired();
assert_eq!(
route_to_apps(&dk, host_msg()),
0,
"stale registration must be swept after TTL"
);
}
#[tokio::test(start_paused = true)]
#[serial_test::serial]
async fn reregister_refreshes_ttl() {
let ns = Namespace::new();
let dk = ns.key(0);
let client = ns.client(0);
let (tx, mut rx) = mpsc::channel::<HostResult>(4);
assert!(register_app(&dk, client, tx.clone()));
tokio::time::advance(REGISTRATION_TTL - std::time::Duration::from_secs(10)).await;
assert!(register_app(&dk, client, tx));
tokio::time::advance(std::time::Duration::from_secs(20)).await;
sweep_expired();
assert_eq!(
route_to_apps(&dk, host_msg()),
1,
"refreshed registration must survive"
);
assert!(rx.try_recv().is_ok());
}
}