use std::time::{Duration, Instant};
use dashmap::DashMap;
use parking_lot::RwLock;
#[derive(Debug, Clone)]
pub struct NodeEntry {
pub node_id: String,
pub last_seen: Instant,
}
#[derive(Default)]
pub struct PrefixIndex {
inner: DashMap<[u8; 32], RwLock<Vec<NodeEntry>>>,
ttl: Duration,
}
impl PrefixIndex {
pub fn new(ttl: Duration) -> Self {
Self {
inner: DashMap::new(),
ttl,
}
}
pub fn insert(&self, digest: [u8; 32], node_id: &str) {
let now = Instant::now();
let entry = self.inner.entry(digest).or_default();
let mut v = entry.write();
if let Some(e) = v.iter_mut().find(|e| e.node_id == node_id) {
e.last_seen = now;
} else {
v.push(NodeEntry {
node_id: node_id.to_string(),
last_seen: now,
});
}
}
pub fn forget_node(&self, node_id: &str) {
for mut e in self.inner.iter_mut() {
e.value_mut().write().retain(|n| n.node_id != node_id);
}
self.inner.retain(|_, v| !v.read().is_empty());
}
pub fn lookup(&self, digest: &[u8; 32]) -> Vec<String> {
let Some(entry) = self.inner.get(digest) else {
return Vec::new();
};
let now = Instant::now();
let g = entry.read();
g.iter()
.filter(|n| now.duration_since(n.last_seen) < self.ttl)
.map(|n| n.node_id.clone())
.collect()
}
pub fn overlap(&self, digests: &[[u8; 32]]) -> std::collections::HashMap<String, usize> {
let mut out: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let now = Instant::now();
for d in digests {
if let Some(entry) = self.inner.get(d) {
let g = entry.read();
for n in g.iter() {
if now.duration_since(n.last_seen) < self.ttl {
*out.entry(n.node_id.clone()).or_insert(0) += 1;
}
}
}
}
out
}
pub fn longest_prefix_overlap(
&self,
digests: &[[u8; 32]],
) -> std::collections::HashMap<String, usize> {
let mut out: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
if digests.is_empty() {
return out;
}
let now = Instant::now();
let first = match self.inner.get(&digests[0]) {
Some(e) => e,
None => return out,
};
let mut active: std::collections::HashSet<String> = first
.read()
.iter()
.filter(|n| now.duration_since(n.last_seen) < self.ttl)
.map(|n| n.node_id.clone())
.collect();
for n in &active {
out.insert(n.clone(), 1);
}
drop(first);
for (i, d) in digests.iter().enumerate().skip(1) {
if active.is_empty() {
break;
}
let holders: std::collections::HashSet<String> = match self.inner.get(d) {
Some(e) => e
.read()
.iter()
.filter(|n| now.duration_since(n.last_seen) < self.ttl)
.map(|n| n.node_id.clone())
.collect(),
None => Default::default(),
};
active.retain(|n| holders.contains(n));
for n in &active {
out.insert(n.clone(), i + 1);
}
}
out
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn d(byte: u8) -> [u8; 32] {
let mut x = [0u8; 32];
x[0] = byte;
x
}
#[test]
fn insert_and_lookup() {
let ix = PrefixIndex::new(Duration::from_secs(60));
ix.insert(d(1), "node-a");
ix.insert(d(1), "node-b");
ix.insert(d(2), "node-a");
assert_eq!(ix.len(), 2);
let mut nodes = ix.lookup(&d(1));
nodes.sort();
assert_eq!(nodes, vec!["node-a".to_string(), "node-b".to_string()]);
}
#[test]
fn forget_node_purges_entries() {
let ix = PrefixIndex::new(Duration::from_secs(60));
ix.insert(d(1), "node-a");
ix.insert(d(2), "node-a");
ix.forget_node("node-a");
assert!(ix.is_empty());
}
#[test]
fn overlap_counts() {
let ix = PrefixIndex::new(Duration::from_secs(60));
ix.insert(d(1), "n1");
ix.insert(d(2), "n1");
ix.insert(d(3), "n2");
let counts = ix.overlap(&[d(1), d(2), d(3), d(4)]);
assert_eq!(counts.get("n1").copied(), Some(2));
assert_eq!(counts.get("n2").copied(), Some(1));
}
#[test]
fn longest_prefix_overlap_walks_in_order() {
let ix = PrefixIndex::new(Duration::from_secs(60));
ix.insert(d(1), "n1");
ix.insert(d(2), "n1");
ix.insert(d(3), "n1");
ix.insert(d(1), "n2");
ix.insert(d(3), "n2");
ix.insert(d(7), "n3");
let req = [d(1), d(2), d(3), d(4)];
let lp = ix.longest_prefix_overlap(&req);
assert_eq!(lp.get("n1").copied(), Some(3));
assert_eq!(lp.get("n2").copied(), Some(1));
assert!(!lp.contains_key("n3"));
}
#[test]
fn longest_prefix_overlap_empty_when_no_chunk0() {
let ix = PrefixIndex::new(Duration::from_secs(60));
ix.insert(d(2), "n1");
ix.insert(d(3), "n1");
let lp = ix.longest_prefix_overlap(&[d(1), d(2), d(3)]);
assert!(lp.is_empty(), "no node holds chunk 0 → no prefix overlap");
}
}