use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use serde::Deserialize;
use tokio::sync::Notify;
use wsb_github::boards::BoardRef;
use wsb_github::sync::Demand;
pub const PAGE_LEASE: Duration = Duration::from_secs(150);
pub const COMMAND_LEASE: Duration = Duration::from_secs(10 * 60);
pub const REFRESH_AT_MOST: Duration = Duration::from_secs(20);
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Subject {
Worktree { id: String },
Repo { repo: String, traffic: bool },
Boards,
Board { owner: String, number: i64 },
Issue { repo: String, number: i64 },
Gists,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Presence {
pub client: String,
pub visible: bool,
#[serde(default)]
pub subjects: Vec<Subject>,
}
#[derive(Debug, Clone)]
struct PageLease {
until: Instant,
shows: Demand,
}
#[derive(Debug, Default)]
struct Leases {
pages: BTreeMap<String, PageLease>,
commands: BTreeMap<PathBuf, Instant>,
refreshed: BTreeMap<String, Instant>,
}
#[derive(Debug, Default)]
pub struct Attention {
leases: Mutex<Leases>,
wake: Arc<Notify>,
}
impl Attention {
pub fn new() -> Self {
Self::default()
}
pub fn wake(&self) -> Arc<Notify> {
self.wake.clone()
}
fn leases(&self) -> std::sync::MutexGuard<'_, Leases> {
self.leases.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn page(&self, client: &str, visible: bool, shows: Demand, now: Instant) {
let changed = {
let mut leases = self.leases();
if visible {
let before = leases.pages.get(client).map(|lease| lease.shows.clone());
leases.pages.insert(client.to_string(), PageLease { until: now + PAGE_LEASE, shows: shows.clone() });
before.as_ref() != Some(&shows)
} else {
leases.pages.remove(client).is_some()
}
};
if changed {
self.wake.notify_one();
}
}
pub fn command(&self, checkout: PathBuf, now: Instant) {
let fresh = self.leases().commands.insert(checkout, now + COMMAND_LEASE).is_none_or(|until| until <= now);
if fresh {
self.wake.notify_one();
}
}
pub fn demand(&self, now: Instant) -> Demand {
let mut leases = self.leases();
leases.pages.retain(|_, lease| lease.until > now);
leases.commands.retain(|_, until| *until > now);
let mut demand = Demand::default();
for lease in leases.pages.values() {
merge(&mut demand, &lease.shows);
demand.notifications = true;
}
demand.checkouts.extend(leases.commands.keys().cloned());
demand
}
pub fn may_refresh(&self, client: &str, now: Instant) -> bool {
let mut leases = self.leases();
leases.refreshed.retain(|_, at| now.duration_since(*at) < REFRESH_AT_MOST);
if leases.refreshed.contains_key(client) {
return false;
}
leases.refreshed.insert(client.to_string(), now);
true
}
}
fn merge(into: &mut Demand, from: &Demand) {
into.checkouts.extend(from.checkouts.iter().cloned());
into.repositories.extend(from.repositories.iter().cloned());
into.traffic.extend(from.traffic.iter().cloned());
into.boards_home |= from.boards_home;
into.boards.extend(from.boards.iter().cloned());
into.threads.extend(from.threads.iter().cloned());
into.gists |= from.gists;
}
pub fn demand_of_subjects(
subjects: &[Subject],
checkout_of: &BTreeMap<String, PathBuf>,
) -> Result<Demand, String> {
let mut demand = Demand::default();
for subject in subjects {
match subject {
Subject::Worktree { id } => {
let path = checkout_of.get(id).ok_or_else(|| format!("no worktree {id} is open here"))?;
demand.checkouts.insert(path.clone());
}
Subject::Repo { repo, traffic } => {
if repo.split('/').count() != 2 || repo.split('/').any(str::is_empty) {
return Err(format!("{repo:?} is not owner/repo"));
}
demand.repositories.insert(repo.clone());
if *traffic {
demand.traffic.insert(repo.clone());
}
}
Subject::Boards => demand.boards_home = true,
Subject::Board { owner, number } => {
demand.boards.insert(BoardRef { owner: owner.clone(), number: *number });
}
Subject::Issue { repo, number } => {
demand.threads.insert((repo.clone(), *number));
}
Subject::Gists => demand.gists = true,
}
}
Ok(demand)
}
#[cfg(test)]
#[path = "attention_tests.rs"]
mod tests;