1use std::time::Duration;
5
6use web_time::Instant;
7
8struct Entry<K, T> {
9 key: K,
10 value: T,
11 visible: bool,
12 deadline: Option<Instant>,
19}
20
21pub struct ExitTransfer<K, T> {
23 key: K,
24 value: T,
25 visible: bool,
26 deadline: Option<Instant>,
27}
28
29impl<K, T> ExitTransfer<K, T> {
30 pub fn key(&self) -> &K {
32 &self.key
33 }
34
35 pub fn value(&self) -> &T {
37 &self.value
38 }
39
40 pub fn is_visible(&self) -> bool {
42 self.visible
43 }
44
45 pub fn into_parts(self) -> (K, T) {
47 (self.key, self.value)
48 }
49}
50
51#[derive(Default)]
58pub struct ExitQueue<K: Eq, T> {
59 entries: Vec<Entry<K, T>>,
60 exit_timeout: Option<Duration>,
61}
62
63impl<K: Eq, T> ExitQueue<K, T> {
64 pub fn new() -> Self {
70 Self {
71 entries: Vec::new(),
72 exit_timeout: None,
73 }
74 }
75
76 pub fn with_exit_timeout(timeout: Duration) -> Self {
83 Self {
84 entries: Vec::new(),
85 exit_timeout: Some(timeout),
86 }
87 }
88
89 pub fn expire(&mut self) -> usize {
94 if self.exit_timeout.is_none() {
95 return 0;
96 }
97 let now = Instant::now();
98 let before = self.entries.len();
99 self.entries
100 .retain(|entry| entry.visible || entry.deadline.is_none_or(|deadline| now < deadline));
101 before - self.entries.len()
102 }
103
104 pub fn transfer_out(&mut self, key: &K) -> Option<ExitTransfer<K, T>> {
110 let index = self.entries.iter().position(|entry| entry.key == *key)?;
111 let entry = self.entries.remove(index);
112 Some(ExitTransfer {
113 key: entry.key,
114 value: entry.value,
115 visible: entry.visible,
116 deadline: entry.deadline,
117 })
118 }
119
120 pub fn adopt(&mut self, transfer: ExitTransfer<K, T>) {
125 self.entries.retain(|entry| entry.key != transfer.key);
126 self.entries.push(Entry {
127 key: transfer.key,
128 value: transfer.value,
129 visible: transfer.visible,
130 deadline: transfer.deadline,
131 });
132 }
133
134 pub fn sync<I>(&mut self, live: I)
139 where
140 K: Clone,
141 I: IntoIterator<Item = (K, T)>,
142 {
143 let now = Instant::now();
144 let deadline = self.exit_timeout.map(|timeout| now + timeout);
145 for entry in &mut self.entries {
146 if entry.visible {
147 entry.deadline = deadline;
150 }
151 entry.visible = false;
152 }
153
154 for (key, value) in live {
155 if let Some(entry) = self.entries.iter_mut().find(|entry| entry.key == key) {
156 entry.value = value;
157 entry.visible = true;
158 entry.deadline = None;
159 } else {
160 self.entries.push(Entry {
161 key,
162 value,
163 visible: true,
164 deadline: None,
165 });
166 }
167 }
168
169 self.expire();
170 }
171
172 pub fn iter(&self) -> impl Iterator<Item = (&K, &T, bool)> + '_ {
177 self.entries
178 .iter()
179 .map(|entry| (&entry.key, &entry.value, entry.visible))
180 }
181
182 pub fn finish(&mut self, key: &K) -> bool {
187 let Some(index) = self
188 .entries
189 .iter()
190 .position(|entry| !entry.visible && entry.key == *key)
191 else {
192 return false;
193 };
194 self.entries.remove(index);
195 true
196 }
197
198 pub fn is_exiting(&self, key: &K) -> bool {
200 self.entries
201 .iter()
202 .any(|entry| !entry.visible && entry.key == *key)
203 }
204
205 pub fn len(&self) -> usize {
207 self.entries.len()
208 }
209
210 pub fn is_empty(&self) -> bool {
212 self.entries.is_empty()
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::ExitQueue;
219
220 fn entries(queue: &ExitQueue<u32, &'static str>) -> Vec<(u32, &'static str, bool)> {
221 queue
222 .iter()
223 .map(|(key, value, visible)| (*key, *value, visible))
224 .collect()
225 }
226
227 #[test]
228 fn sync_marks_removed_entries_exiting_without_dropping_them() {
229 let mut queue = ExitQueue::new();
230 queue.sync([(1, "one"), (2, "two")]);
231 queue.sync([(2, "updated")]);
232
233 assert_eq!(
234 entries(&queue),
235 vec![(1, "one", false), (2, "updated", true)]
236 );
237 assert!(queue.is_exiting(&1));
238 assert!(!queue.is_exiting(&2));
239 assert_eq!(queue.len(), 2);
240 }
241
242 #[test]
243 fn readding_an_exiting_key_resurrects_the_existing_entry() {
244 let mut queue = ExitQueue::new();
245 queue.sync([(7, "old")]);
246 queue.sync([]);
247 queue.sync([(7, "new")]);
248
249 assert_eq!(entries(&queue), vec![(7, "new", true)]);
250 assert!(!queue.is_exiting(&7));
251 assert_eq!(queue.len(), 1);
252 }
253
254 #[test]
255 fn duplicate_live_keys_do_not_double_insert() {
256 let mut queue = ExitQueue::new();
257 queue.sync([(3, "first"), (3, "last")]);
258
259 assert_eq!(entries(&queue), vec![(3, "last", true)]);
260 }
261
262 #[test]
263 fn an_exit_timeout_releases_entries_whose_animation_never_completes() {
264 let mut queue: ExitQueue<u32, &'static str> =
266 ExitQueue::with_exit_timeout(std::time::Duration::from_millis(10));
267 queue.sync([(1, "one"), (2, "two")]);
268 queue.sync([(2, "two")]);
269 assert!(queue.is_exiting(&1));
270
271 std::thread::sleep(std::time::Duration::from_millis(25));
272 queue.sync([(2, "two")]);
273
274 assert!(!queue.is_exiting(&1));
275 assert_eq!(entries(&queue), vec![(2, "two", true)]);
276 }
277
278 #[test]
279 fn repeated_syncs_do_not_extend_an_exiting_entrys_deadline() {
280 let mut queue: ExitQueue<u32, &'static str> =
281 ExitQueue::with_exit_timeout(std::time::Duration::from_millis(20));
282 queue.sync([(1, "one")]);
283 queue.sync([]);
284
285 for _ in 0..4 {
286 std::thread::sleep(std::time::Duration::from_millis(8));
287 queue.sync([]);
288 }
289
290 assert!(queue.is_empty(), "deadline must not be pushed forward");
291 }
292
293 #[test]
294 fn transfer_preserves_exit_progress_across_collections() {
295 let mut source: ExitQueue<u32, &'static str> =
296 ExitQueue::with_exit_timeout(std::time::Duration::from_millis(500));
297 let mut target: ExitQueue<u32, &'static str> =
298 ExitQueue::with_exit_timeout(std::time::Duration::from_millis(500));
299 source.sync([(1, "one")]);
300 source.sync([]);
301 assert!(source.is_exiting(&1));
302
303 let moved = source.transfer_out(&1).expect("entry exists");
304 assert!(!moved.is_visible());
305 target.adopt(moved);
306
307 assert!(source.is_empty());
308 assert!(target.is_exiting(&1));
310 assert_eq!(entries(&target), vec![(1, "one", false)]);
311 }
312
313 #[test]
314 fn finish_removes_only_the_exiting_entry() {
315 let mut queue = ExitQueue::new();
316 queue.sync([(1, "one"), (2, "two")]);
317 queue.sync([(2, "two")]);
318
319 assert!(!queue.finish(&2));
320 assert!(queue.finish(&1));
321 assert!(!queue.finish(&1));
322 assert_eq!(entries(&queue), vec![(2, "two", true)]);
323 }
324}