use dashmap::DashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::time::{Duration, Instant};
const DEDUP_TTL: Duration = Duration::from_millis(100);
pub struct DedupStore {
inner: DashMap<u64, Instant>,
ttl: Duration,
}
impl DedupStore {
pub fn new() -> Self {
Self {
inner: DashMap::new(),
ttl: DEDUP_TTL,
}
}
pub fn check_and_insert(
&self,
session_id: &str,
tool_name: &str,
tool_input: &serde_json::Value,
) -> bool {
let key = self.compute_hash(session_id, tool_name, tool_input);
let now = Instant::now();
if let Some(entry) = self.inner.get(&key) {
if now.duration_since(*entry.value()) < self.ttl {
return true; }
}
self.inner.insert(key, now);
false
}
fn compute_hash(
&self,
session_id: &str,
tool_name: &str,
tool_input: &serde_json::Value,
) -> u64 {
let mut hasher = DefaultHasher::new();
session_id.hash(&mut hasher);
tool_name.hash(&mut hasher);
hash_value(tool_input, &mut hasher);
hasher.finish()
}
pub fn evict_expired(&self) {
let now = Instant::now();
self.inner.retain(|_, v| now.duration_since(*v) < self.ttl);
}
}
impl Default for DedupStore {
fn default() -> Self {
Self::new()
}
}
fn hash_value(value: &serde_json::Value, hasher: &mut impl Hasher) {
match value {
serde_json::Value::Null => 0u8.hash(hasher),
serde_json::Value::Bool(b) => {
1u8.hash(hasher);
b.hash(hasher);
}
serde_json::Value::Number(n) => {
2u8.hash(hasher);
format!("{n}").hash(hasher);
}
serde_json::Value::String(s) => {
3u8.hash(hasher);
s.hash(hasher);
}
serde_json::Value::Array(arr) => {
4u8.hash(hasher);
arr.len().hash(hasher);
for item in arr {
hash_value(item, hasher);
}
}
serde_json::Value::Object(map) => {
5u8.hash(hasher);
map.len().hash(hasher);
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
key.hash(hasher);
hash_value(&map[key], hasher);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_first_event_is_not_duplicate() {
let store = DedupStore::new();
let input = json!({"command": "ls -la"});
let is_dup = store.check_and_insert("session-1", "bash", &input);
assert!(!is_dup, "first occurrence must not be a duplicate");
}
#[test]
fn test_same_event_within_ttl_is_duplicate() {
let store = DedupStore::new();
let input = json!({"command": "ls -la"});
let first = store.check_and_insert("session-1", "bash", &input);
let second = store.check_and_insert("session-1", "bash", &input);
assert!(!first, "first occurrence must not be a duplicate");
assert!(second, "immediate repeat must be detected as duplicate");
}
#[test]
fn test_same_event_after_ttl_is_not_duplicate() {
let store = DedupStore::new();
let input = json!({"command": "ls -la"});
store.check_and_insert("session-1", "bash", &input);
std::thread::sleep(Duration::from_millis(150));
let after_ttl = store.check_and_insert("session-1", "bash", &input);
assert!(!after_ttl, "event after TTL expiry must not be a duplicate");
}
#[test]
fn test_different_events_are_not_duplicates() {
let store = DedupStore::new();
let first = store.check_and_insert("session-1", "bash", &json!({"cmd": "ls"}));
let second =
store.check_and_insert("session-1", "read_file", &json!({"path": "/etc/hosts"}));
assert!(!first, "first event must not be a duplicate");
assert!(!second, "different event must not be a duplicate");
}
#[test]
fn test_different_sessions_same_tool_not_duplicate() {
let store = DedupStore::new();
let input = json!({"command": "ls"});
store.check_and_insert("session-A", "bash", &input);
let second = store.check_and_insert("session-B", "bash", &input);
assert!(
!second,
"same tool call in different session must not be a duplicate"
);
}
#[test]
fn test_evict_expired_removes_old_entries() {
let store = DedupStore::new();
let input = json!({"key": "value"});
store.check_and_insert("session-1", "bash", &input);
assert!(
store.check_and_insert("session-1", "bash", &input),
"entry must exist before eviction"
);
std::thread::sleep(Duration::from_millis(150));
store.evict_expired();
let after_evict = store.check_and_insert("session-1", "bash", &input);
assert!(
!after_evict,
"evicted entry must not be a duplicate on next check"
);
}
#[test]
fn test_dedup_key_order_independent() {
let store = DedupStore::new();
let mut map_a = serde_json::Map::new();
map_a.insert("command".to_string(), json!("ls -la"));
map_a.insert("path".to_string(), json!("/tmp"));
let input_a = serde_json::Value::Object(map_a);
let mut map_b = serde_json::Map::new();
map_b.insert("path".to_string(), json!("/tmp"));
map_b.insert("command".to_string(), json!("ls -la"));
let input_b = serde_json::Value::Object(map_b);
let first = store.check_and_insert("session-1", "bash", &input_a);
let second = store.check_and_insert("session-1", "bash", &input_b);
assert!(!first, "first occurrence must not be a duplicate");
assert!(
second,
"same logical event with different key order must be detected as duplicate"
);
}
#[test]
fn test_hash_value_sorts_nested_keys() {
let input_a = json!({"z": {"b": 2, "a": 1}, "a": [{"y": 1, "x": 2}]});
let input_b = json!({"a": [{"x": 2, "y": 1}], "z": {"a": 1, "b": 2}});
let mut hasher_a = DefaultHasher::new();
let mut hasher_b = DefaultHasher::new();
super::hash_value(&input_a, &mut hasher_a);
super::hash_value(&input_b, &mut hasher_b);
assert_eq!(
hasher_a.finish(),
hasher_b.finish(),
"nested keys in different order must produce the same hash"
);
}
}