use std::collections::VecDeque;
pub const PENDING_MS: i64 = 10 * 60_000;
pub const MAX_PENDING: usize = 64;
pub const START_GRACE_MS: i64 = 10_000;
pub const MAX_TOOL: usize = 64;
#[derive(Clone, Debug, Default)]
pub struct Hooked {
pending: VecDeque<(String, i64)>,
pub last_ms: Option<i64>,
}
impl Hooked {
pub fn heard(&mut self, now: i64) {
self.last_ms = Some(now);
}
pub fn before(&mut self, tool: &str, now: i64) {
self.heard(now);
self.expire(now);
if self.pending.len() >= MAX_PENDING {
self.pending.pop_front();
}
self.pending.push_back((tool.to_string(), now));
}
pub fn take(&mut self, tool: &str, now: i64) -> bool {
self.expire(now);
match self.pending.iter().position(|(t, _)| t == tool) {
Some(i) => {
self.pending.remove(i);
true
}
None => false,
}
}
fn expire(&mut self, now: i64) {
while self
.pending
.front()
.is_some_and(|(_, at)| now.saturating_sub(*at) > PENDING_MS)
{
self.pending.pop_front();
}
}
#[cfg(test)]
fn waiting(&self) -> Vec<&str> {
self.pending.iter().map(|(t, _)| t.as_str()).collect()
}
}
pub fn valid_tool(tool: &str) -> bool {
!tool.is_empty()
&& tool.len() <= MAX_TOOL
&& tool
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_call_takes_one_event_of_its_own_tool_in_order() {
let mut h = Hooked::default();
assert!(!h.take("propose", 0), "nothing heard");
h.before("lease", 1);
h.before("propose", 2);
h.before("lease", 3);
assert!(!h.take("status", 4), "another tool's event is no proof");
assert!(h.take("lease", 5));
assert_eq!(h.waiting(), ["propose", "lease"], "the oldest went");
assert!(h.take("lease", 5) && h.take("propose", 5));
assert!(!h.take("propose", 5), "one event, one call");
assert_eq!(h.last_ms, Some(3));
}
#[test]
fn an_event_waits_for_its_call_only_so_long() {
let mut h = Hooked::default();
h.before("propose", 1000);
assert!(h.take("propose", 1000 + PENDING_MS), "at the limit");
h.before("propose", 1000);
h.before("lease", 2000);
assert!(!h.take("propose", 1001 + PENDING_MS), "past it");
assert!(
h.take("lease", 1001 + PENDING_MS),
"a later one still waits"
);
h.before("a", 0);
h.before("b", PENDING_MS + 1);
assert_eq!(h.waiting(), ["b"], "an event clears what expired");
}
#[test]
fn a_session_keeps_only_so_many_events() {
let mut h = Hooked::default();
h.before("first", 0);
for _ in 1..MAX_PENDING {
h.before("x", 0);
}
assert_eq!(h.waiting().len(), MAX_PENDING);
h.before("last", 0);
assert_eq!(h.waiting().len(), MAX_PENDING);
assert!(!h.take("first", 0), "the oldest made room");
assert!(h.take("last", 0));
}
#[test]
fn hearing_anything_counts_as_alive() {
let mut h = Hooked::default();
assert_eq!(h.last_ms, None);
h.heard(7);
assert_eq!(h.last_ms, Some(7));
assert!(h.waiting().is_empty());
}
#[test]
fn a_tool_is_a_short_lowercase_name() {
assert!(valid_tool("propose") && valid_tool("change_status") && valid_tool("a1"));
let long = "a".repeat(MAX_TOOL);
assert!(valid_tool(&long));
assert!(!valid_tool(&format!("{long}a")));
for bad in ["", "Propose", "mcp__lonca__x-y", "a b", "a\n", "รค"] {
assert!(!valid_tool(bad), "{bad:?}");
}
}
}