brows3r_lib/search/cancel.rs
1//! Lightweight cancellation token for long-running search operations.
2//!
3//! `CancellationToken` is an `Arc<AtomicBool>` wrapper. The `cancel()` call
4//! sets the flag; the background task polls `is_cancelled()` between pages and
5//! exits early when the flag is set.
6//!
7//! This deliberately avoids `tokio_util::sync::CancellationToken` to keep the
8//! dependency surface small — the atomics-based approach is sufficient for a
9//! single-flag stop signal.
10//!
11//! OCP: wrapping the `Arc<AtomicBool>` in this struct means the token shape
12//! can be extended (e.g. adding a reason code) without changing call sites.
13
14use std::sync::{
15 atomic::{AtomicBool, Ordering},
16 Arc,
17};
18
19// ---------------------------------------------------------------------------
20// CancellationToken
21// ---------------------------------------------------------------------------
22
23/// A cloneable, thread-safe cancellation flag.
24///
25/// Clone the token to share it between the producer (registry) and the
26/// consumer (background task). Calling `cancel()` on any clone sets the flag
27/// for all clones.
28#[derive(Clone, Default)]
29pub struct CancellationToken {
30 atomic: Arc<AtomicBool>,
31}
32
33impl CancellationToken {
34 /// Create a new, uncancelled token.
35 pub fn new() -> Self {
36 Self {
37 atomic: Arc::new(AtomicBool::new(false)),
38 }
39 }
40
41 /// Signal cancellation. Idempotent — safe to call multiple times.
42 pub fn cancel(&self) {
43 self.atomic.store(true, Ordering::Relaxed);
44 }
45
46 /// Returns `true` once `cancel()` has been called on any clone.
47 pub fn is_cancelled(&self) -> bool {
48 self.atomic.load(Ordering::Relaxed)
49 }
50}
51
52// ---------------------------------------------------------------------------
53// Tests
54// ---------------------------------------------------------------------------
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn new_token_is_not_cancelled() {
62 let token = CancellationToken::new();
63 assert!(!token.is_cancelled());
64 }
65
66 #[test]
67 fn cancel_sets_flag() {
68 let token = CancellationToken::new();
69 token.cancel();
70 assert!(token.is_cancelled());
71 }
72
73 #[test]
74 fn cancel_is_idempotent() {
75 let token = CancellationToken::new();
76 token.cancel();
77 token.cancel(); // second call must not panic
78 assert!(token.is_cancelled());
79 }
80
81 #[test]
82 fn clone_shares_flag() {
83 let token = CancellationToken::new();
84 let clone = token.clone();
85 token.cancel();
86 assert!(clone.is_cancelled(), "clone must see the cancellation");
87 }
88
89 #[test]
90 fn cancel_on_clone_visible_on_original() {
91 let token = CancellationToken::new();
92 let clone = token.clone();
93 clone.cancel();
94 assert!(
95 token.is_cancelled(),
96 "original must see clone's cancellation"
97 );
98 }
99}