use std::collections::BTreeMap;
use crate::discord::ActivityInfo;
type ActivityKey = (String, i64);
#[derive(Default)]
pub(super) struct ActivityRegistry {
entries: BTreeMap<ActivityKey, (ActivityInfo, u64)>,
next_sequence: u64,
}
impl ActivityRegistry {
pub(super) fn set(&mut self, client_id: String, pid: i64, activity: ActivityInfo) -> u64 {
let sequence = self.next_sequence;
self.next_sequence = sequence.saturating_add(1);
self.entries.insert((client_id, pid), (activity, sequence));
sequence
}
pub(super) fn clear(&mut self, client_id: &str, pid: i64) {
self.entries.remove(&(client_id.to_owned(), pid));
}
pub(super) fn clear_if_current(&mut self, client_id: &str, pid: i64, sequence: u64) {
if self
.entries
.get(&(client_id.to_owned(), pid))
.is_some_and(|(_, current)| *current == sequence)
{
self.clear(client_id, pid);
}
}
pub(super) fn activities(&self) -> Vec<ActivityInfo> {
let mut entries: Vec<(ActivityInfo, u64)> = self.entries.values().cloned().collect();
entries.sort_by(|(_, a), (_, b)| b.cmp(a));
entries.into_iter().map(|(activity, _)| activity).collect()
}
pub(super) fn activity_for_client(&self, client_id: &str) -> Option<ActivityInfo> {
self.entries
.iter()
.find(|((id, _pid), _)| id == client_id)
.map(|(_, (activity, _))| activity.clone())
}
pub(super) fn latest_activity(&self) -> Option<ActivityInfo> {
self.entries
.iter()
.max_by_key(|(_, (_, sequence))| *sequence)
.map(|(_, (activity, _))| activity.clone())
}
}
#[cfg(test)]
mod tests {
use super::ActivityRegistry;
use crate::discord::{ActivityInfo, ActivityKind};
#[test]
fn registry_aggregates_and_clears_per_app() {
let mut registry = ActivityRegistry::default();
registry.set(
"app-a".to_owned(),
1,
ActivityInfo::test(ActivityKind::Playing, "Game A"),
);
registry.set(
"app-b".to_owned(),
2,
ActivityInfo::test(ActivityKind::Listening, "Song B"),
);
let activities = registry.activities();
let names: Vec<&str> = activities
.iter()
.map(|activity| activity.name.as_str())
.collect();
assert_eq!(names, ["Song B", "Game A"]);
registry.set(
"app-a".to_owned(),
1,
ActivityInfo::test(ActivityKind::Playing, "Game A2"),
);
registry.clear("app-b", 2);
let activities = registry.activities();
let names: Vec<&str> = activities
.iter()
.map(|activity| activity.name.as_str())
.collect();
assert_eq!(names, ["Game A2"]);
}
#[test]
fn activity_for_client_returns_latest_by_client_id() {
let mut registry = ActivityRegistry::default();
registry.set(
"vscode".to_owned(),
1,
ActivityInfo::test(ActivityKind::Playing, "Editing a.rs"),
);
assert_eq!(
registry
.activity_for_client("vscode")
.map(|activity| activity.name),
Some("Editing a.rs".to_owned())
);
registry.set(
"vscode".to_owned(),
1,
ActivityInfo::test(ActivityKind::Playing, "Editing b.rs"),
);
assert_eq!(
registry
.activity_for_client("vscode")
.map(|activity| activity.name),
Some("Editing b.rs".to_owned())
);
assert!(registry.activity_for_client("unknown").is_none());
}
#[test]
fn latest_activity_returns_most_recent_update() {
let mut registry = ActivityRegistry::default();
registry.set(
"vscode".to_owned(),
1,
ActivityInfo::test(ActivityKind::Playing, "Editing a.rs"),
);
assert_eq!(
registry.latest_activity().map(|activity| activity.name),
Some("Editing a.rs".to_owned())
);
registry.set(
"game".to_owned(),
2,
ActivityInfo::test(ActivityKind::Playing, "Epic Game"),
);
assert_eq!(
registry.latest_activity().map(|activity| activity.name),
Some("Epic Game".to_owned())
);
registry.set(
"vscode".to_owned(),
1,
ActivityInfo::test(ActivityKind::Playing, "Editing b.rs"),
);
assert_eq!(
registry.latest_activity().map(|activity| activity.name),
Some("Editing b.rs".to_owned())
);
registry.clear("vscode", 1);
assert_eq!(
registry.latest_activity().map(|activity| activity.name),
Some("Epic Game".to_owned())
);
registry.clear("game", 2);
assert!(registry.latest_activity().is_none());
}
#[test]
fn stale_connection_cleanup_keeps_the_reconnected_activity() {
let mut registry = ActivityRegistry::default();
let old = registry.set("app".to_owned(), 1, ActivityInfo::playing("Old activity"));
let new = registry.set("app".to_owned(), 1, ActivityInfo::playing("New activity"));
registry.clear_if_current("app", 1, old);
assert_eq!(
registry.latest_activity(),
Some(ActivityInfo::playing("New activity"))
);
registry.clear_if_current("app", 1, new);
assert!(registry.latest_activity().is_none());
}
}