use std::path::Path;
use crate::types::Task;
pub(super) const BOARD_MIN_COOLDOWN_SECS: i64 = 10;
const BOARD_REPEAT_LIMIT: u32 = 1;
const FORCE_ESCALATION_LIMIT: u32 = 3;
const FORCE_ESCALATION_WINDOW_SECS: i64 = 120;
#[derive(Debug, PartialEq, Eq)]
pub(super) enum AntiPollStatus { Allowed(u32), Cooldown(i64), Repeat(u32), ForceBlocked }
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct ForceMarkerState {
pub count: u32,
pub window_start: i64,
}
pub(super) fn task_fingerprint(tasks: &[Task]) -> String {
let mut parts: Vec<String> = tasks.iter().map(|t| format!("{}:{}", t.id, t.status.label())).collect();
parts.sort();
parts.join(",")
}
pub(super) fn watch_instead_of_polling_hint(tasks: &[Task]) -> String {
use crate::types::TaskStatus;
let running_ids: Vec<&str> = tasks.iter().filter(|task| task.status == TaskStatus::Running).map(|task| task.id.as_str()).collect();
if running_ids.is_empty() {
return "Use `aid watch --wait <id>` instead of polling.".to_string();
}
if running_ids.len() == 1 {
return format!("Use `aid watch --wait {}` instead of polling.", running_ids[0]);
}
let commands = running_ids.iter().map(|task_id| format!("`aid watch --wait {task_id}`")).collect::<Vec<_>>().join(", ");
format!("Use one of {commands} instead of polling.")
}
pub(super) fn anti_poll_status(marker_path: &Path, fingerprint: &str, now: i64, force: bool) -> (AntiPollStatus, ForceMarkerState) {
let marker = read_board_marker(marker_path);
let elapsed = now - marker.timestamp;
if force {
let force_state = next_force_state(&marker, now);
if is_force_window_active(marker.force_window_start, now) && marker.force_count >= FORCE_ESCALATION_LIMIT {
return (AntiPollStatus::ForceBlocked, force_state);
}
return (AntiPollStatus::Allowed(0), force_state);
}
if marker.is_uninitialized() {
return (AntiPollStatus::Allowed(0), ForceMarkerState::default());
}
let force_state = ForceMarkerState { count: marker.force_count, window_start: marker.force_window_start };
if marker.fingerprint == fingerprint {
let repeat_count = marker.repeat_count + 1;
if repeat_count >= BOARD_REPEAT_LIMIT { return (AntiPollStatus::Repeat(repeat_count), force_state) }
return (AntiPollStatus::Allowed(repeat_count), force_state);
}
if elapsed >= 0 && elapsed < BOARD_MIN_COOLDOWN_SECS {
return (AntiPollStatus::Cooldown(elapsed), force_state);
}
(AntiPollStatus::Allowed(0), force_state)
}
#[derive(Debug, Default)]
struct BoardMarker {
timestamp: i64,
fingerprint: String,
repeat_count: u32,
force_count: u32,
force_window_start: i64,
}
impl BoardMarker {
fn is_uninitialized(&self) -> bool {
self.timestamp == 0 && self.fingerprint.is_empty() && self.repeat_count == 0 && self.force_count == 0 && self.force_window_start == 0
}
}
fn read_board_marker(marker_path: &Path) -> BoardMarker {
let Ok(prev) = std::fs::read_to_string(marker_path) else { return BoardMarker::default() };
let parts: Vec<&str> = prev.lines().collect();
BoardMarker {
timestamp: parts.first().and_then(|s| s.parse().ok()).unwrap_or(0),
fingerprint: parts.get(1).copied().unwrap_or("").to_string(),
repeat_count: parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
force_count: parts.get(3).and_then(|s| s.parse().ok()).unwrap_or(0),
force_window_start: parts.get(4).and_then(|s| s.parse().ok()).unwrap_or(0),
}
}
fn next_force_state(marker: &BoardMarker, now: i64) -> ForceMarkerState {
if is_force_window_active(marker.force_window_start, now) {
if marker.force_count >= FORCE_ESCALATION_LIMIT {
return ForceMarkerState { count: marker.force_count, window_start: marker.force_window_start };
}
return ForceMarkerState { count: marker.force_count + 1, window_start: marker.force_window_start };
}
ForceMarkerState { count: 1, window_start: now }
}
fn is_force_window_active(force_window_start: i64, now: i64) -> bool {
force_window_start > 0 && now - force_window_start >= 0 && now - force_window_start < FORCE_ESCALATION_WINDOW_SECS
}
pub(super) fn write_board_marker(marker_path: &Path, fingerprint: &str, now: i64, repeat_count: u32, force_count: u32, force_window_start: i64) {
let _ = std::fs::write(marker_path, format!("{now}\n{fingerprint}\n{repeat_count}\n{force_count}\n{force_window_start}"));
}