use std::time::Duration;
use web_time::Instant;
struct Entry<K, T> {
key: K,
value: T,
visible: bool,
deadline: Option<Instant>,
}
pub struct ExitTransfer<K, T> {
key: K,
value: T,
visible: bool,
deadline: Option<Instant>,
}
impl<K, T> ExitTransfer<K, T> {
pub fn key(&self) -> &K {
&self.key
}
pub fn value(&self) -> &T {
&self.value
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn into_parts(self) -> (K, T) {
(self.key, self.value)
}
}
#[derive(Default)]
pub struct ExitQueue<K: Eq, T> {
entries: Vec<Entry<K, T>>,
exit_timeout: Option<Duration>,
}
impl<K: Eq, T> ExitQueue<K, T> {
pub fn new() -> Self {
Self {
entries: Vec::new(),
exit_timeout: None,
}
}
pub fn with_exit_timeout(timeout: Duration) -> Self {
Self {
entries: Vec::new(),
exit_timeout: Some(timeout),
}
}
pub fn expire(&mut self) -> usize {
if self.exit_timeout.is_none() {
return 0;
}
let now = Instant::now();
let before = self.entries.len();
self.entries
.retain(|entry| entry.visible || entry.deadline.is_none_or(|deadline| now < deadline));
before - self.entries.len()
}
pub fn transfer_out(&mut self, key: &K) -> Option<ExitTransfer<K, T>> {
let index = self.entries.iter().position(|entry| entry.key == *key)?;
let entry = self.entries.remove(index);
Some(ExitTransfer {
key: entry.key,
value: entry.value,
visible: entry.visible,
deadline: entry.deadline,
})
}
pub fn adopt(&mut self, transfer: ExitTransfer<K, T>) {
self.entries.retain(|entry| entry.key != transfer.key);
self.entries.push(Entry {
key: transfer.key,
value: transfer.value,
visible: transfer.visible,
deadline: transfer.deadline,
});
}
pub fn sync<I>(&mut self, live: I)
where
K: Clone,
I: IntoIterator<Item = (K, T)>,
{
let now = Instant::now();
let deadline = self.exit_timeout.map(|timeout| now + timeout);
for entry in &mut self.entries {
if entry.visible {
entry.deadline = deadline;
}
entry.visible = false;
}
for (key, value) in live {
if let Some(entry) = self.entries.iter_mut().find(|entry| entry.key == key) {
entry.value = value;
entry.visible = true;
entry.deadline = None;
} else {
self.entries.push(Entry {
key,
value,
visible: true,
deadline: None,
});
}
}
self.expire();
}
pub fn iter(&self) -> impl Iterator<Item = (&K, &T, bool)> + '_ {
self.entries
.iter()
.map(|entry| (&entry.key, &entry.value, entry.visible))
}
pub fn finish(&mut self, key: &K) -> bool {
let Some(index) = self
.entries
.iter()
.position(|entry| !entry.visible && entry.key == *key)
else {
return false;
};
self.entries.remove(index);
true
}
pub fn is_exiting(&self, key: &K) -> bool {
self.entries
.iter()
.any(|entry| !entry.visible && entry.key == *key)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::ExitQueue;
fn entries(queue: &ExitQueue<u32, &'static str>) -> Vec<(u32, &'static str, bool)> {
queue
.iter()
.map(|(key, value, visible)| (*key, *value, visible))
.collect()
}
#[test]
fn sync_marks_removed_entries_exiting_without_dropping_them() {
let mut queue = ExitQueue::new();
queue.sync([(1, "one"), (2, "two")]);
queue.sync([(2, "updated")]);
assert_eq!(
entries(&queue),
vec![(1, "one", false), (2, "updated", true)]
);
assert!(queue.is_exiting(&1));
assert!(!queue.is_exiting(&2));
assert_eq!(queue.len(), 2);
}
#[test]
fn readding_an_exiting_key_resurrects_the_existing_entry() {
let mut queue = ExitQueue::new();
queue.sync([(7, "old")]);
queue.sync([]);
queue.sync([(7, "new")]);
assert_eq!(entries(&queue), vec![(7, "new", true)]);
assert!(!queue.is_exiting(&7));
assert_eq!(queue.len(), 1);
}
#[test]
fn duplicate_live_keys_do_not_double_insert() {
let mut queue = ExitQueue::new();
queue.sync([(3, "first"), (3, "last")]);
assert_eq!(entries(&queue), vec![(3, "last", true)]);
}
#[test]
fn an_exit_timeout_releases_entries_whose_animation_never_completes() {
let mut queue: ExitQueue<u32, &'static str> =
ExitQueue::with_exit_timeout(std::time::Duration::from_millis(10));
queue.sync([(1, "one"), (2, "two")]);
queue.sync([(2, "two")]);
assert!(queue.is_exiting(&1));
std::thread::sleep(std::time::Duration::from_millis(25));
queue.sync([(2, "two")]);
assert!(!queue.is_exiting(&1));
assert_eq!(entries(&queue), vec![(2, "two", true)]);
}
#[test]
fn repeated_syncs_do_not_extend_an_exiting_entrys_deadline() {
let mut queue: ExitQueue<u32, &'static str> =
ExitQueue::with_exit_timeout(std::time::Duration::from_millis(20));
queue.sync([(1, "one")]);
queue.sync([]);
for _ in 0..4 {
std::thread::sleep(std::time::Duration::from_millis(8));
queue.sync([]);
}
assert!(queue.is_empty(), "deadline must not be pushed forward");
}
#[test]
fn transfer_preserves_exit_progress_across_collections() {
let mut source: ExitQueue<u32, &'static str> =
ExitQueue::with_exit_timeout(std::time::Duration::from_millis(500));
let mut target: ExitQueue<u32, &'static str> =
ExitQueue::with_exit_timeout(std::time::Duration::from_millis(500));
source.sync([(1, "one")]);
source.sync([]);
assert!(source.is_exiting(&1));
let moved = source.transfer_out(&1).expect("entry exists");
assert!(!moved.is_visible());
target.adopt(moved);
assert!(source.is_empty());
assert!(target.is_exiting(&1));
assert_eq!(entries(&target), vec![(1, "one", false)]);
}
#[test]
fn finish_removes_only_the_exiting_entry() {
let mut queue = ExitQueue::new();
queue.sync([(1, "one"), (2, "two")]);
queue.sync([(2, "two")]);
assert!(!queue.finish(&2));
assert!(queue.finish(&1));
assert!(!queue.finish(&1));
assert_eq!(entries(&queue), vec![(2, "two", true)]);
}
}