1use std::cell::RefCell;
6use std::collections::HashMap;
7use std::rc::Rc;
8use crate::{Cursor, DataProvider, Graph, NeighborResult, NodeId, QueryParams};
9
10pub type CacheKey = (NodeId, Option<Cursor>);
11
12#[derive(Debug, Clone, PartialEq)]
18enum Slot { InFlight(u64), Failed(String) }
19
20#[derive(Default)]
22pub struct NeighborCache {
23 ready: HashMap<String, NeighborResult>,
24 slots: HashMap<String, Slot>,
25 wanted: Vec<CacheKey>,
26 dirty: bool,
27 failures: Vec<(CacheKey, String)>,
30 new_arrivals: Vec<CacheKey>,
33 next_epoch: u64,
35 settled: HashMap<String, u64>,
41 discard_before: Option<u64>,
45}
46
47fn key_str(k: &CacheKey) -> String {
59 match &k.1 {
60 Some(Cursor(c)) => format!("{}\u{1}{}\u{1}1{}", k.0.len(), k.0, c),
61 None => format!("{}\u{1}{}\u{1}0", k.0.len(), k.0),
62 }
63}
64
65impl NeighborCache {
66 pub fn new() -> Self { Self::default() }
67
68 pub fn get(&self, k: &CacheKey) -> Option<&NeighborResult> { self.ready.get(&key_str(k)) }
69
70 pub fn error(&self, k: &CacheKey) -> Option<&str> {
71 match self.slots.get(&key_str(k)) {
72 Some(Slot::Failed(e)) => Some(e.as_str()),
73 _ => None,
74 }
75 }
76
77 pub fn mark_wanted(&mut self, k: CacheKey) {
81 let ks = key_str(&k);
82 if self.ready.contains_key(&ks) || self.slots.contains_key(&ks) { return; }
83 if self.wanted.iter().any(|w| key_str(w) == ks) { return; }
84 self.wanted.push(k);
85 }
86
87 pub fn take_wanted(&mut self) -> Vec<(CacheKey, u64)> {
94 let out: Vec<CacheKey> = self.wanted.drain(..).collect();
95 out.into_iter()
96 .map(|k| {
97 self.next_epoch += 1;
98 self.slots.insert(key_str(&k), Slot::InFlight(self.next_epoch));
99 (k, self.next_epoch)
100 })
101 .collect()
102 }
103
104 fn is_stale(&self, ks: &str, epoch: u64) -> bool {
111 if matches!(self.discard_before, Some(floor) if epoch <= floor) {
116 return true;
117 }
118 if let Some(Slot::InFlight(e)) = self.slots.get(ks) {
119 if *e > epoch {
120 return true;
121 }
122 }
123 matches!(self.settled.get(ks), Some(s) if *s >= epoch)
124 }
125
126 pub fn fill(&mut self, k: CacheKey, epoch: u64, mut res: NeighborResult) {
133 let ks = key_str(&k);
134 if self.is_stale(&ks, epoch) { return; }
135 res.pending = false;
142 self.slots.remove(&ks);
143 self.ready.insert(ks.clone(), res);
144 self.settled.insert(ks, epoch);
145 self.new_arrivals.push(k);
146 self.dirty = true;
147 }
148
149 pub fn fail(&mut self, k: CacheKey, epoch: u64, err: String) {
151 let ks = key_str(&k);
152 if !matches!(self.slots.get(&ks), Some(Slot::InFlight(e)) if *e == epoch) {
159 return;
160 }
161 self.slots.insert(ks.clone(), Slot::Failed(err.clone()));
162 self.settled.insert(ks, epoch);
166 self.failures.push((k, err));
167 self.dirty = true;
168 }
169
170 pub fn cancel(&mut self, k: &CacheKey) {
176 let ks = key_str(k);
177 self.wanted.retain(|w| key_str(w) != ks);
178 if matches!(self.slots.get(&ks), Some(Slot::InFlight(_))) {
182 self.slots.remove(&ks);
183 }
184 }
185
186 pub fn is_in_flight(&self, k: &CacheKey) -> bool {
189 matches!(self.slots.get(&key_str(k)), Some(Slot::InFlight(_)))
190 }
191
192 pub fn take_dirty(&mut self) -> bool { std::mem::take(&mut self.dirty) }
194
195 pub fn take_failures(&mut self) -> Vec<(CacheKey, String)> {
198 std::mem::take(&mut self.failures)
199 }
200
201 pub fn take_new_arrivals(&mut self) -> Vec<CacheKey> {
204 std::mem::take(&mut self.new_arrivals)
205 }
206
207 pub fn clear_failures(&mut self) {
212 self.slots.retain(|_, s| !matches!(s, Slot::Failed(_)));
213 }
214
215 pub fn reset(&mut self) {
236 self.ready.clear();
237 self.slots.clear();
238 self.wanted.clear();
239 self.dirty = false;
240 self.failures.clear();
241 self.new_arrivals.clear();
242 self.settled.clear();
243 self.discard_before = Some(self.next_epoch);
244 }
245
246 pub fn is_loading(&self) -> bool {
247 !self.wanted.is_empty() || self.slots.values().any(|s| matches!(s, Slot::InFlight(_)))
248 }
249
250 pub fn ready_results(&self) -> impl Iterator<Item = &NeighborResult> { self.ready.values() }
253}
254
255pub struct CachingProvider {
259 base: Graph,
260 cache: Rc<RefCell<NeighborCache>>,
261}
262
263impl CachingProvider {
264 pub fn new(base: Graph, cache: Rc<RefCell<NeighborCache>>) -> Self {
265 Self { base, cache }
266 }
267
268 pub fn cache(&self) -> Rc<RefCell<NeighborCache>> { self.cache.clone() }
269}
270
271impl DataProvider for CachingProvider {
272 fn load(&self) -> Graph { self.base.clone() }
273
274 fn neighbors(&self, focus: &NodeId, params: &QueryParams) -> NeighborResult {
275 let k: CacheKey = (focus.clone(), params.cursor.clone());
276 if let Some(hit) = self.cache.borrow().get(&k) {
277 return hit.clone();
278 }
279 if self.cache.borrow().error(&k).is_some() {
284 return NeighborResult {
285 nodes: vec![], edges: vec![], aggregates: vec![], next: None, pending: false,
286 };
287 }
288 self.cache.borrow_mut().mark_wanted(k);
289 NeighborResult {
290 nodes: vec![], edges: vec![], aggregates: vec![], next: None, pending: true,
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::Node;
299
300 fn key(id: &str) -> CacheKey { (id.to_string(), None) }
301
302 fn result_with(id: &str) -> NeighborResult {
303 NeighborResult {
304 nodes: vec![Node { id: id.into(), label: None, attrs: Default::default() }],
305 edges: vec![], aggregates: vec![], next: None, pending: false,
306 }
307 }
308
309 #[test]
310 fn miss_queues_the_key_and_reports_loading() {
311 let mut c = NeighborCache::new();
312 assert!(c.get(&key("a")).is_none());
313 c.mark_wanted(key("a"));
314 assert!(c.is_loading());
315 assert_eq!(c.take_wanted(), vec![(key("a"), 1)]);
316 }
317
318 #[test]
319 fn in_flight_key_is_not_queued_twice() {
320 let mut c = NeighborCache::new();
321 c.mark_wanted(key("a"));
322 c.mark_wanted(key("a")); assert_eq!(c.take_wanted().len(), 1);
324 c.mark_wanted(key("a")); assert!(c.take_wanted().is_empty(), "in-flight key must not re-queue");
326 }
327
328 #[test]
329 fn fill_stores_result_clears_pending_and_sets_dirty() {
330 let mut c = NeighborCache::new();
331 c.mark_wanted(key("a"));
332 let epoch = c.take_wanted()[0].1;
333 c.fill(key("a"), epoch, result_with("a"));
334 assert!(c.take_dirty());
335 assert!(!c.take_dirty(), "dirty is consumed");
336 assert!(!c.is_loading());
337 assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "a");
338 }
339
340 #[test]
341 fn failure_is_recorded_and_does_not_retry_loop() {
342 let mut c = NeighborCache::new();
343 c.mark_wanted(key("a"));
344 let epoch = c.take_wanted()[0].1;
345 c.fail(key("a"), epoch, "boom".into());
346 assert!(c.take_dirty());
347 assert!(!c.is_loading());
348 assert_eq!(c.error(&key("a")), Some("boom"));
349 c.mark_wanted(key("a"));
350 assert!(c.take_wanted().is_empty(), "failed key must not auto-retry");
351 }
352
353 #[test]
354 fn distinct_cursors_are_distinct_cache_entries() {
355 let mut c = NeighborCache::new();
356 let k2 = ("a".to_string(), Some(Cursor("off:8".into())));
357 c.fill(key("a"), 0, result_with("a"));
358 assert!(c.get(&k2).is_none(), "cursor is part of the key");
359 }
360
361 #[test]
362 fn ready_results_yields_every_filled_entry() {
363 let mut c = NeighborCache::new();
364 c.fill(key("a"), 0, result_with("n1"));
365 c.fill(("b".to_string(), Some(Cursor("off:8".into()))), 0, result_with("n2"));
366 let mut ids: Vec<String> =
367 c.ready_results().map(|r| r.nodes[0].id.clone()).collect();
368 ids.sort();
369 assert_eq!(ids, vec!["n1".to_string(), "n2".to_string()]);
370 }
371
372 #[test]
373 fn provider_returns_pending_on_miss_and_queues_it() {
374 let cache = Rc::new(RefCell::new(NeighborCache::new()));
375 let p = CachingProvider::new(Graph::default(), cache.clone());
376 let r = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
377 assert!(r.pending);
378 assert!(r.nodes.is_empty());
379 assert_eq!(cache.borrow_mut().take_wanted(), vec![(key("a"), 1)]);
380 }
381
382 #[test]
383 fn provider_returns_cached_result_once_filled() {
384 let cache = Rc::new(RefCell::new(NeighborCache::new()));
385 let p = CachingProvider::new(Graph::default(), cache.clone());
386 cache.borrow_mut().fill(key("a"), 0, result_with("n1"));
387 let r = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
388 assert!(!r.pending);
389 assert_eq!(r.nodes[0].id, "n1");
390 }
391
392 #[test]
393 fn provider_does_not_queue_a_hit() {
394 let cache = Rc::new(RefCell::new(NeighborCache::new()));
395 let p = CachingProvider::new(Graph::default(), cache.clone());
396 cache.borrow_mut().fill(key("a"), 0, result_with("n1"));
397 let _ = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
398 assert!(cache.borrow_mut().take_wanted().is_empty());
399 }
400
401 #[test]
402 fn provider_reports_a_failed_key_as_resolved_and_empty_not_pending() {
403 let cache = Rc::new(RefCell::new(NeighborCache::new()));
406 let p = CachingProvider::new(Graph::default(), cache.clone());
407 cache.borrow_mut().mark_wanted(key("a"));
408 let epoch = cache.borrow_mut().take_wanted()[0].1;
409 cache.borrow_mut().fail(key("a"), epoch, "boom".into());
410
411 let r = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
412 assert!(!r.pending, "a failed key must not look pending");
413 assert!(r.nodes.is_empty());
414 assert!(r.next.is_none());
415 assert!(cache.borrow_mut().take_wanted().is_empty(), "must not queue a failed key");
416 }
417
418 #[test]
419 fn clear_failures_lets_a_failed_key_be_queued_again() {
420 let mut c = NeighborCache::new();
421 c.mark_wanted(key("a"));
422 let epoch = c.take_wanted()[0].1;
423 c.fail(key("a"), epoch, "boom".into());
424 c.mark_wanted(key("a"));
425 assert!(c.take_wanted().is_empty(), "no auto-retry before an explicit clear");
426
427 c.clear_failures();
428 assert_eq!(c.error(&key("a")), None);
429 c.mark_wanted(key("a"));
430 let requeued = c.take_wanted();
431 assert_eq!(requeued.len(), 1, "retry re-queues the key");
432 assert_eq!(requeued[0].0, key("a"));
433 }
434
435 #[test]
436 fn clear_failures_leaves_ready_and_in_flight_alone() {
437 let mut c = NeighborCache::new();
438 c.fill(key("ready"), 0, result_with("n1"));
439
440 c.mark_wanted(key("inflight"));
441 let _ = c.take_wanted();
442
443 c.mark_wanted(key("bad"));
444 let epoch = c.take_wanted()[0].1;
445 c.fail(key("bad"), epoch, "boom".into());
446
447 c.clear_failures();
448 assert!(c.get(&key("ready")).is_some(), "ready entries survive");
449 assert!(c.is_loading(), "the in-flight slot survives");
450 c.mark_wanted(key("inflight"));
451 assert!(c.take_wanted().is_empty(), "still in flight, so still not re-queued");
452 }
453
454 #[test]
455 fn fill_never_trusts_pending_from_the_wire() {
456 let cache = Rc::new(RefCell::new(NeighborCache::new()));
457 let mut res = result_with("n1");
458 res.pending = true; cache.borrow_mut().fill(key("a"), 0, res);
460 assert!(!cache.borrow().get(&key("a")).unwrap().pending, "pending is client-side only");
461
462 let p = CachingProvider::new(Graph::default(), cache.clone());
465 let r = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
466 assert!(!r.pending);
467 assert_eq!(r.nodes[0].id, "n1");
468 }
469
470 #[test]
471 fn new_arrivals_drain_once_and_report_each_filled_key() {
472 let mut c = NeighborCache::new();
473 let k2 = ("b".to_string(), Some(Cursor("off:8".into())));
474 c.fill(key("a"), 0, result_with("n1"));
475 c.fill(k2.clone(), 0, result_with("n2"));
476
477 let mut drained = c.take_new_arrivals();
478 drained.sort_by(|x, y| x.0.cmp(&y.0));
479 assert_eq!(drained, vec![key("a"), k2]);
480 assert!(c.take_new_arrivals().is_empty(), "draining consumes the arrivals");
481 }
482
483 #[test]
484 fn ids_containing_the_separator_do_not_collide_with_cursor_keys() {
485 let mut c = NeighborCache::new();
488 let k1 = ("a\u{1}b".to_string(), None);
489 let k2 = ("a".to_string(), Some(Cursor("b".into())));
490 c.fill(k1.clone(), 0, result_with("from-k1"));
491 c.fill(k2.clone(), 0, result_with("from-k2"));
492 assert_eq!(c.get(&k1).unwrap().nodes[0].id, "from-k1");
493 assert_eq!(c.get(&k2).unwrap().nodes[0].id, "from-k2");
494 }
495
496 #[test]
497 fn failures_are_drainable_once() {
498 let mut c = NeighborCache::new();
499 c.mark_wanted(key("a"));
500 let epoch = c.take_wanted()[0].1;
501 c.fail(key("a"), epoch, "boom".into());
502
503 let drained = c.take_failures();
504 assert_eq!(drained.len(), 1);
505 assert_eq!(drained[0].0, key("a"), "structured key is preserved");
506 assert_eq!(drained[0].1, "boom");
507
508 assert!(c.take_failures().is_empty(), "draining consumes the failures");
509 assert_eq!(c.error(&key("a")), Some("boom"));
511 c.mark_wanted(key("a"));
512 assert!(c.take_wanted().is_empty());
513 }
514
515 #[test]
516 fn failure_with_a_cursor_keeps_its_cursor_in_the_drained_key() {
517 let mut c = NeighborCache::new();
518 let k = ("a".to_string(), Some(Cursor("grp:X:0".into())));
519 c.mark_wanted(k.clone());
520 let epoch = c.take_wanted()[0].1;
521 c.fail(k.clone(), epoch, "nope".into());
522 let drained = c.take_failures();
523 assert_eq!(drained[0].0, k);
524 }
525
526 #[test]
527 fn cancel_clears_the_in_flight_slot_and_stops_reporting_loading() {
528 let mut c = NeighborCache::new();
529 c.mark_wanted(key("a"));
530 let _ = c.take_wanted();
531 assert!(c.is_loading());
532
533 c.cancel(&key("a"));
534 assert!(!c.is_loading(), "a cancelled fetch must not keep the spinner up");
535 assert!(!c.is_in_flight(&key("a")));
536 }
537
538 #[test]
539 fn fail_after_cancel_records_nothing() {
540 let mut c = NeighborCache::new();
544 c.mark_wanted(key("a"));
545 let epoch = c.take_wanted()[0].1;
546 c.cancel(&key("a"));
547
548 c.fail(key("a"), epoch, "AbortError".into());
549 assert_eq!(c.error(&key("a")), None, "no failure is recorded");
550 assert!(c.take_failures().is_empty(), "nothing is reported to the host");
551 assert!(!c.take_dirty(), "a cancellation is not a repaint-worthy change");
552 }
553
554 #[test]
555 fn a_cancelled_key_can_be_requested_again() {
556 let mut c = NeighborCache::new();
557 c.mark_wanted(key("a"));
558 let epoch = c.take_wanted()[0].1;
559 c.cancel(&key("a"));
560 c.fail(key("a"), epoch, "AbortError".into());
561
562 c.mark_wanted(key("a"));
563 let requeued = c.take_wanted();
564 assert_eq!(requeued.len(), 1, "cancellation is not sticky");
565 assert_eq!(requeued[0].0, key("a"));
566 }
567
568 #[test]
569 fn fill_after_cancel_still_stores_the_result() {
570 let mut c = NeighborCache::new();
576 c.mark_wanted(key("a"));
577 let epoch = c.take_wanted()[0].1;
578 c.cancel(&key("a"));
579
580 c.fill(key("a"), epoch, result_with("n1"));
581 assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "n1");
582 assert!(!c.is_in_flight(&key("a")));
583 }
584
585 #[test]
586 fn cancel_drops_a_key_queued_but_not_yet_drained() {
587 let mut c = NeighborCache::new();
588 c.mark_wanted(key("a"));
589 c.cancel(&key("a"));
590 assert!(c.take_wanted().is_empty(), "a queued key is dequeued, not fetched");
591 assert!(!c.is_loading());
592 }
593
594 #[test]
595 fn cancel_of_an_unknown_key_is_a_no_op() {
596 let mut c = NeighborCache::new();
597 c.cancel(&key("ghost"));
598 assert!(!c.is_loading());
599 c.mark_wanted(key("ghost"));
601 let epoch = c.take_wanted()[0].1;
602 c.fail(key("ghost"), epoch, "boom".into());
603 assert_eq!(c.error(&key("ghost")), Some("boom"));
604 }
605
606 #[test]
607 fn is_in_flight_is_true_only_between_take_wanted_and_settle() {
608 let mut c = NeighborCache::new();
609 c.mark_wanted(key("a"));
610 assert!(!c.is_in_flight(&key("a")), "queued is not yet in flight");
611 let epoch = c.take_wanted()[0].1;
612 assert!(c.is_in_flight(&key("a")));
613 c.fill(key("a"), epoch, result_with("n1"));
614 assert!(!c.is_in_flight(&key("a")));
615 }
616
617 #[test]
618 fn cache_keys_are_hashable_so_hosts_can_index_by_them() {
619 use std::collections::HashMap;
621 let mut m: HashMap<CacheKey, u32> = HashMap::new();
622 m.insert(key("a"), 1);
623 m.insert(("a".to_string(), Some(Cursor("off:8".into()))), 2);
624 assert_eq!(m.get(&key("a")), Some(&1));
625 assert_eq!(m.len(), 2, "the cursor is part of the identity");
626 }
627
628 #[test]
631 fn cancel_requeue_cancel_does_not_poison_the_key() {
632 let mut c = NeighborCache::new();
638 c.mark_wanted(key("a"));
639 let epoch1 = c.take_wanted()[0].1; c.cancel(&key("a"));
641
642 c.mark_wanted(key("a"));
643 let epoch2 = c.take_wanted()[0].1; c.cancel(&key("a"));
645
646 c.fail(key("a"), epoch1, "AbortError".into()); c.fail(key("a"), epoch2, "AbortError".into()); assert_eq!(c.error(&key("a")), None, "neither cancelled attempt records a failure");
650 c.mark_wanted(key("a"));
651 assert_eq!(c.take_wanted().len(), 1, "the key is not poisoned and can be requested again");
652 }
653
654 #[test]
655 fn genuine_failure_on_a_re_requested_attempt_after_cancel_is_recorded() {
656 let mut c = NeighborCache::new();
661 c.mark_wanted(key("a"));
662 let epoch1 = c.take_wanted()[0].1; c.cancel(&key("a"));
664
665 c.mark_wanted(key("a"));
666 let epoch2 = c.take_wanted()[0].1; assert!(c.is_loading());
668
669 c.fail(key("a"), epoch1, "boom".into()); assert!(c.is_loading(), "P2 is still outstanding");
671 assert_eq!(c.error(&key("a")), None);
672
673 c.fail(key("a"), epoch2, "boom".into()); assert!(!c.is_loading(), "no orphaned slot: is_loading must go quiet");
675 assert_eq!(c.error(&key("a")), Some("boom"));
676 }
677
678 #[test]
679 fn cancel_of_an_already_failed_key_leaves_the_failure_intact() {
680 let mut c = NeighborCache::new();
684 c.mark_wanted(key("a"));
685 let epoch = c.take_wanted()[0].1;
686 c.fail(key("a"), epoch, "boom".into());
687
688 c.cancel(&key("a")); assert_eq!(c.error(&key("a")), Some("boom"), "cancel must not erase a recorded failure");
691 c.mark_wanted(key("a"));
692 assert!(c.take_wanted().is_empty(), "must not re-queue a failed key via cancel's back door");
693 }
694
695 #[test]
696 fn stale_fill_from_a_superseded_attempt_is_discarded() {
697 let mut c = NeighborCache::new();
698 c.mark_wanted(key("a"));
699 let epoch1 = c.take_wanted()[0].1; c.cancel(&key("a"));
701
702 c.mark_wanted(key("a"));
703 let epoch2 = c.take_wanted()[0].1; c.fill(key("a"), epoch1, result_with("stale")); assert!(c.get(&key("a")).is_none(), "a stale fill must not overwrite the live attempt");
707 assert!(c.is_in_flight(&key("a")), "P2 is still outstanding");
708
709 c.fill(key("a"), epoch2, result_with("live"));
710 assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "live");
711 }
712
713 #[test]
714 fn cancel_of_a_ready_key_is_a_no_op() {
715 let mut c = NeighborCache::new();
716 c.fill(key("a"), 0, result_with("n1"));
717 c.cancel(&key("a"));
718 assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "n1", "a ready result is untouched by cancel");
719 c.mark_wanted(key("a"));
720 assert!(c.take_wanted().is_empty(), "a ready key is still a hit, not re-queued");
721 }
722
723 #[test]
724 fn absent_cursor_and_empty_cursor_are_distinct_cache_entries() {
725 let mut c = NeighborCache::new();
726 let empty_cursor_key = ("a".to_string(), Some(Cursor(String::new())));
727 c.fill(key("a"), 0, result_with("no-cursor"));
728 c.fill(empty_cursor_key.clone(), 0, result_with("empty-cursor"));
729
730 assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "no-cursor");
731 assert_eq!(c.get(&empty_cursor_key).unwrap().nodes[0].id, "empty-cursor");
732 }
733
734 #[test]
737 fn late_stale_success_does_not_overwrite_a_newer_settled_success() {
738 let mut c = NeighborCache::new();
742 c.mark_wanted(key("b"));
743 let epoch1 = c.take_wanted()[0].1; c.cancel(&key("b"));
745
746 c.mark_wanted(key("b"));
747 let epoch2 = c.take_wanted()[0].1; c.fill(key("b"), epoch2, result_with("new")); let _ = c.take_new_arrivals();
751
752 c.fill(key("b"), epoch1, result_with("old")); assert_eq!(
754 c.get(&key("b")).unwrap().nodes[0].id,
755 "new",
756 "a stale success must not overwrite a newer one"
757 );
758 assert!(
759 c.take_new_arrivals().is_empty(),
760 "a discarded stale fill must not be reported as a new arrival"
761 );
762 }
763
764 #[test]
765 fn late_stale_success_does_not_erase_a_recorded_failure() {
766 let mut c = NeighborCache::new();
772 c.mark_wanted(key("a"));
773 let epoch1 = c.take_wanted()[0].1; c.cancel(&key("a"));
775
776 c.mark_wanted(key("a"));
777 let epoch2 = c.take_wanted()[0].1; c.fail(key("a"), epoch2, "boom".into()); let _ = c.take_failures();
781 assert_eq!(c.error(&key("a")), Some("boom"));
782
783 c.fill(key("a"), epoch1, result_with("late")); assert_eq!(
785 c.error(&key("a")),
786 Some("boom"),
787 "a stale success must not erase a recorded failure"
788 );
789 assert!(c.get(&key("a")).is_none(), "the failed key must not also look like a hit");
790 }
791
792 #[test]
793 fn fill_after_cancel_with_nothing_settled_since_still_stores() {
794 let mut c = NeighborCache::new();
798 c.mark_wanted(key("a"));
799 let epoch = c.take_wanted()[0].1;
800 c.cancel(&key("a"));
801
802 c.fill(key("a"), epoch, result_with("n1"));
803 assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "n1");
804 }
805
806 #[test]
809 fn reset_clears_everything_the_cache_holds() {
810 let mut c = NeighborCache::new();
811
812 c.mark_wanted(key("ready"));
814 let e_ready = c.take_wanted()[0].1;
815 c.fill(key("ready"), e_ready, result_with("n1"));
816 c.mark_wanted(key("bad"));
818 let e_bad = c.take_wanted()[0].1;
819 c.fail(key("bad"), e_bad, "boom".into());
820 c.mark_wanted(key("inflight"));
822 let _ = c.take_wanted();
823 c.mark_wanted(key("queued"));
824
825 c.reset();
826
827 assert!(c.get(&key("ready")).is_none(), "ready entries describe the old graph");
828 assert_eq!(c.error(&key("bad")), None, "failures are not carried across a load");
829 assert!(!c.is_loading(), "no in-flight slots and nothing queued survive");
830 assert!(!c.take_dirty(), "the dirty flag is cleared");
831 assert!(c.take_failures().is_empty(), "undrained failures are dropped");
832 assert!(c.take_new_arrivals().is_empty(), "undrained arrivals are dropped");
833 assert_eq!(c.ready_results().count(), 0);
834 }
835
836 #[test]
837 fn a_key_settled_before_a_reset_can_be_requested_again() {
838 let mut c = NeighborCache::new();
842 c.mark_wanted(key("w0"));
843 let epoch = c.take_wanted()[0].1;
844 c.fill(key("w0"), epoch, result_with("old-neighbor"));
845 assert!(c.get(&key("w0")).is_some());
846
847 c.reset();
848
849 c.mark_wanted(key("w0"));
850 let requeued = c.take_wanted();
851 assert_eq!(requeued.len(), 1, "a reused id must fetch again, not hit");
852 assert_eq!(requeued[0].0, key("w0"));
853
854 c.fill(key("w0"), requeued[0].1, result_with("new-neighbor"));
855 assert_eq!(c.get(&key("w0")).unwrap().nodes[0].id, "new-neighbor");
856 }
857
858 #[test]
859 fn a_key_that_failed_before_a_reset_is_not_a_permanent_dead_end() {
860 let mut c = NeighborCache::new();
861 c.mark_wanted(key("w0"));
862 let epoch = c.take_wanted()[0].1;
863 c.fail(key("w0"), epoch, "boom".into());
864 c.mark_wanted(key("w0"));
865 assert!(c.take_wanted().is_empty(), "failed keys do not auto-retry pre-reset");
866
867 c.reset();
868
869 c.mark_wanted(key("w0"));
870 assert_eq!(c.take_wanted().len(), 1, "the new dataset's node is fetchable");
871 }
872
873 #[test]
874 fn a_fetch_outstanding_across_a_reset_cannot_settle_into_the_new_dataset() {
875 let mut c = NeighborCache::new();
880 c.mark_wanted(key("w0"));
881 let old_epoch = c.take_wanted()[0].1;
882
883 c.reset();
884
885 c.fill(key("w0"), old_epoch, result_with("old-neighbor"));
886 assert!(c.get(&key("w0")).is_none(), "an old-dataset answer must not land");
887 assert!(c.take_new_arrivals().is_empty(), "nor be reported as an arrival");
888
889 c.fail(key("w0"), old_epoch, "boom".into());
890 assert_eq!(c.error(&key("w0")), None, "nor poison the reused id with its failure");
891 }
892
893 #[test]
894 fn epochs_keep_climbing_across_a_reset() {
895 let mut c = NeighborCache::new();
899 c.mark_wanted(key("a"));
900 let before = c.take_wanted()[0].1;
901
902 c.reset();
903
904 c.mark_wanted(key("a"));
905 let after = c.take_wanted()[0].1;
906 assert!(after > before, "epochs are monotonic across a reset ({after} > {before})");
907 }
908
909 #[test]
910 fn reset_on_a_fresh_cache_imposes_no_floor_on_later_attempts() {
911 let mut c = NeighborCache::new();
914 c.reset();
915
916 c.mark_wanted(key("a"));
917 let epoch = c.take_wanted()[0].1;
918 c.fill(key("a"), epoch, result_with("n1"));
919 assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "n1");
920 }
921
922 #[test]
923 fn duplicate_fill_with_the_same_epoch_is_discarded() {
924 let mut c = NeighborCache::new();
928 c.mark_wanted(key("a"));
929 let epoch = c.take_wanted()[0].1;
930 c.fill(key("a"), epoch, result_with("first"));
931 let _ = c.take_new_arrivals();
932
933 c.fill(key("a"), epoch, result_with("duplicate"));
934 assert_eq!(
935 c.get(&key("a")).unwrap().nodes[0].id,
936 "first",
937 "a duplicate delivery of the same attempt does not overwrite"
938 );
939 assert!(
940 c.take_new_arrivals().is_empty(),
941 "a duplicate settle is not reported as a new arrival"
942 );
943 }
944}