Skip to main content

almost_enough/
lib.rs

1//! # almost-enough
2//!
3//! Cooperative cancellation for Rust. Cancel long-running operations from another thread.
4//!
5//! ## Which Crate?
6//!
7//! - **Application code**: Use `almost-enough` (this crate) - has all implementations
8//! - **Library code**: Depend on [`enough`](https://docs.rs/enough) (minimal) and accept `impl Stop`
9//!
10//! ## Complete Example
11//!
12//! ```rust
13//! # #[cfg(feature = "alloc")]
14//! # fn main() {
15//! use almost_enough::{Stopper, Stop};
16//! use std::thread;
17//! use std::time::Duration;
18//!
19//! // 1. Create a stopper
20//! let stop = Stopper::new();
21//!
22//! // 2. Clone and pass to worker thread
23//! let worker_stop = stop.clone();
24//! let handle = thread::spawn(move || {
25//!     for i in 0..1000 {
26//!         // 3. Check periodically - exit early if cancelled
27//!         if worker_stop.should_stop() {
28//!             return Err("cancelled");
29//!         }
30//!         // ... do work ...
31//!         # std::hint::black_box(i);
32//!     }
33//!     Ok("completed")
34//! });
35//!
36//! // 4. Cancel from main thread (or signal handler, timeout, etc.)
37//! thread::sleep(Duration::from_millis(1));
38//! stop.cancel();
39//!
40//! // Worker exits early
41//! let result = handle.join().unwrap();
42//! // result is either Ok("completed") or Err("cancelled")
43//! # }
44//! # #[cfg(not(feature = "alloc"))]
45//! # fn main() {}
46//! ```
47//!
48//! ## Quick Reference
49//!
50//! ```rust,no_run
51//! # #[cfg(feature = "alloc")]
52//! # fn main() {
53//! # use almost_enough::{Stopper, Stop, StopReason};
54//! # fn example() -> Result<(), StopReason> {
55//! let stop = Stopper::new();
56//! stop.cancel();              // Trigger cancellation
57//! stop.should_stop();         // Returns true if cancelled
58//! stop.check()?;              // Returns Err(StopReason) if cancelled
59//! # Ok(())
60//! # }
61//! # }
62//! # #[cfg(not(feature = "alloc"))]
63//! # fn main() {}
64//! ```
65//!
66//! ## Type Overview
67//!
68//! | Type | Feature | Use Case |
69//! |------|---------|----------|
70//! | [`Unstoppable`] | core | Zero-cost "never stop" |
71//! | [`StopSource`] / [`StopRef`] | core | Stack-based, borrowed, zero-alloc |
72//! | [`FnStop`] | core | Wrap any closure |
73//! | [`OrStop`] | core | Combine multiple stops |
74//! | [`Stopper`] | alloc | **Default choice** - Arc-based, clone to share |
75//! | [`SyncStopper`] | alloc | Like Stopper with Acquire/Release ordering |
76//! | [`ChildStopper`] | alloc | Hierarchical parent-child cancellation |
77//! | [`StopToken`] | alloc | **Type-erased dynamic dispatch** - Arc-based, `Clone` |
78//! | [`BoxedStop`] | alloc | Type-erased (prefer `StopToken`) |
79//! | [`WithTimeout`] | std | Add deadline to any `Stop` |
80//! | [`DebouncedTimeout`] | std | Like `WithTimeout`, skips most clock reads |
81//!
82//! ## StopExt Extension Trait
83//!
84//! The [`StopExt`] trait adds combinator methods to any [`Stop`] implementation:
85//!
86//! ```rust
87//! use almost_enough::{StopSource, Stop, StopExt};
88//!
89//! let timeout = StopSource::new();
90//! let cancel = StopSource::new();
91//!
92//! // Combine: stop if either stops
93//! let combined = timeout.as_ref().or(cancel.as_ref());
94//! assert!(!combined.should_stop());
95//!
96//! cancel.cancel();
97//! assert!(combined.should_stop());
98//! ```
99//!
100//! ## Type Erasure with `StopToken`
101//!
102//! [`StopToken`] wraps `Arc<dyn Stop>` — it's `Clone` (cheap Arc increment),
103//! type-erased, and can be sent across threads. [`Stopper`] and
104//! [`SyncStopper`] convert to `StopToken` at zero cost via `From`/`Into`
105//! (the existing Arc is reused, no double-wrapping).
106//!
107//! Use [`CloneStop`] (a trait alias for `Stop + Clone + 'static`) to accept
108//! any clonable stop, then erase with `into_token()` at the boundary:
109//!
110//! ```rust
111//! # #[cfg(feature = "alloc")]
112//! # fn main() {
113//! use almost_enough::{CloneStop, StopToken, Stopper, Stop, StopExt};
114//!
115//! fn outer(stop: impl CloneStop) {
116//!     // Erase the concrete type — StopToken is Clone
117//!     let stop: StopToken = stop.into_token();
118//!     inner(&stop);
119//! }
120//!
121//! fn inner(stop: &StopToken) {
122//!     let stop2 = stop.clone(); // cheap Arc increment
123//!     // Only one version of this function exists
124//!     while !stop.should_stop() {
125//!         break;
126//!     }
127//! }
128//!
129//! let stop = Stopper::new();
130//! outer(stop);
131//! # }
132//! # #[cfg(not(feature = "alloc"))]
133//! # fn main() {}
134//! ```
135//!
136//! ## Hierarchical Cancellation with `.child()`
137//!
138//! Create child stops that inherit cancellation from their parent:
139//!
140//! ```rust
141//! # #[cfg(feature = "alloc")]
142//! # fn main() {
143//! use almost_enough::{Stopper, Stop, StopExt};
144//!
145//! let parent = Stopper::new();
146//! let child = parent.child();
147//!
148//! // Child cancellation doesn't affect parent
149//! child.cancel();
150//! assert!(!parent.should_stop());
151//!
152//! // But parent cancellation propagates to children
153//! let child2 = parent.child();
154//! parent.cancel();
155//! assert!(child2.should_stop());
156//! # }
157//! # #[cfg(not(feature = "alloc"))]
158//! # fn main() {}
159//! ```
160//!
161//! ## Stop Guards (RAII Cancellation)
162//!
163//! Automatically stop on scope exit unless explicitly disarmed:
164//!
165//! ```rust
166//! # #[cfg(feature = "alloc")]
167//! # fn main() {
168//! use almost_enough::{Stopper, StopDropRoll};
169//!
170//! fn do_work(source: &Stopper) -> Result<(), &'static str> {
171//!     let guard = source.stop_on_drop();
172//!
173//!     // If we return early or panic, source is stopped
174//!     risky_operation()?;
175//!
176//!     // Success! Don't stop.
177//!     guard.disarm();
178//!     Ok(())
179//! }
180//!
181//! fn risky_operation() -> Result<(), &'static str> {
182//!     Ok(())
183//! }
184//!
185//! let source = Stopper::new();
186//! do_work(&source).unwrap();
187//! # }
188//! # #[cfg(not(feature = "alloc"))]
189//! # fn main() {}
190//! ```
191//!
192//! ## Feature Flags
193//!
194//! - **`std`** (default) - Full functionality including timeouts
195//! - **`alloc`** - Arc-based types, `into_boxed()`, `child()`, `StopDropRoll`
196//! - **None** - Core trait and stack-based types only
197
198#![cfg_attr(not(feature = "std"), no_std)]
199#![forbid(unsafe_code)]
200#![warn(missing_docs)]
201#![warn(clippy::all)]
202
203#[cfg(feature = "alloc")]
204extern crate alloc;
205
206// Re-export everything from enough
207#[allow(deprecated)]
208pub use enough::{Never, Stop, StopReason, Unstoppable};
209
210/// Trait alias for stop tokens that can be cloned and sent across threads.
211///
212/// This is `Stop + Clone + 'static` — the minimum needed to clone a stop
213/// token and send it to other threads. Since `Stop` already requires
214/// `Send + Sync`, `CloneStop` types are fully thread-safe.
215///
216/// Use `impl CloneStop` in public API signatures when you need to clone
217/// the stop token, then erase with [`StopToken`] internally:
218///
219/// ```rust
220/// # #[cfg(feature = "alloc")]
221/// # fn main() {
222/// use almost_enough::{CloneStop, StopToken, Stop};
223///
224/// pub fn parallel_work(stop: impl CloneStop) {
225///     let stop = StopToken::new(stop);
226///     let s2 = stop.clone(); // Arc increment
227/// }
228/// # }
229/// # #[cfg(not(feature = "alloc"))]
230/// # fn main() {}
231/// ```
232pub trait CloneStop: Stop + Clone + 'static {}
233
234/// Blanket implementation: any `Stop + Clone + 'static` is `CloneStop`.
235impl<T: Stop + Clone + 'static> CloneStop for T {}
236
237// Core modules (no_std, no alloc)
238mod func;
239mod or;
240mod source;
241
242pub use func::FnStop;
243pub use or::OrStop;
244pub use source::{StopRef, StopSource};
245
246// Alloc-dependent modules
247#[cfg(feature = "alloc")]
248mod boxed;
249#[cfg(feature = "alloc")]
250mod stopper;
251#[cfg(feature = "alloc")]
252mod sync_stopper;
253#[cfg(feature = "alloc")]
254mod tree;
255
256#[cfg(feature = "alloc")]
257pub use boxed::BoxedStop;
258#[cfg(feature = "alloc")]
259mod stop_token;
260#[cfg(feature = "alloc")]
261pub use stop_token::StopToken;
262#[cfg(feature = "alloc")]
263pub use stopper::Stopper;
264#[cfg(feature = "alloc")]
265pub use sync_stopper::SyncStopper;
266#[cfg(feature = "alloc")]
267pub use tree::ChildStopper;
268
269// Std-dependent modules
270#[cfg(feature = "std")]
271pub mod time;
272#[cfg(feature = "std")]
273pub use time::{DebouncedTimeout, DebouncedTimeoutExt, TimeoutExt, WithTimeout};
274
275// Cancel guard module
276#[cfg(feature = "alloc")]
277mod guard;
278#[cfg(feature = "alloc")]
279pub use guard::{CancelGuard, Cancellable, StopDropRoll};
280
281/// Extension trait providing ergonomic combinators for [`Stop`] implementations.
282///
283/// This trait is automatically implemented for all `Stop + Sized` types.
284///
285/// # Example
286///
287/// ```rust
288/// use almost_enough::{StopSource, Stop, StopExt};
289///
290/// let source_a = StopSource::new();
291/// let source_b = StopSource::new();
292///
293/// // Combine with .or()
294/// let combined = source_a.as_ref().or(source_b.as_ref());
295///
296/// assert!(!combined.should_stop());
297///
298/// source_b.cancel();
299/// assert!(combined.should_stop());
300/// ```
301pub trait StopExt: Stop + Sized {
302    /// Combine this stop with another, stopping if either stops.
303    ///
304    /// This is equivalent to `OrStop::new(self, other)` but with a more
305    /// ergonomic method syntax that allows chaining.
306    ///
307    /// # Example
308    ///
309    /// ```rust
310    /// use almost_enough::{StopSource, Stop, StopExt};
311    ///
312    /// let timeout = StopSource::new();
313    /// let cancel = StopSource::new();
314    ///
315    /// let combined = timeout.as_ref().or(cancel.as_ref());
316    /// assert!(!combined.should_stop());
317    ///
318    /// cancel.cancel();
319    /// assert!(combined.should_stop());
320    /// ```
321    ///
322    /// # Chaining
323    ///
324    /// Multiple sources can be chained:
325    ///
326    /// ```rust
327    /// use almost_enough::{StopSource, Stop, StopExt};
328    ///
329    /// let a = StopSource::new();
330    /// let b = StopSource::new();
331    /// let c = StopSource::new();
332    ///
333    /// let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
334    ///
335    /// c.cancel();
336    /// assert!(combined.should_stop());
337    /// ```
338    #[inline]
339    fn or<S: Stop>(self, other: S) -> OrStop<Self, S> {
340        OrStop::new(self, other)
341    }
342
343    /// Convert this stop into a boxed trait object.
344    ///
345    /// This is useful for preventing monomorphization at API boundaries.
346    /// Instead of generating a new function for each `impl Stop` type,
347    /// you can erase the type to `BoxedStop` and have a single implementation.
348    ///
349    /// # Example
350    ///
351    /// ```rust
352    /// # #[cfg(feature = "alloc")]
353    /// # fn main() {
354    /// use almost_enough::{Stopper, BoxedStop, Stop, StopExt};
355    ///
356    /// // This function is monomorphized for each Stop type
357    /// fn process_generic(stop: impl Stop + 'static) {
358    ///     // Erase type at boundary
359    ///     process_concrete(stop.into_boxed());
360    /// }
361    ///
362    /// // This function has only one implementation
363    /// fn process_concrete(stop: BoxedStop) {
364    ///     while !stop.should_stop() {
365    ///         break;
366    ///     }
367    /// }
368    ///
369    /// let stop = Stopper::new();
370    /// process_generic(stop);
371    /// # }
372    /// # #[cfg(not(feature = "alloc"))]
373    /// # fn main() {}
374    /// ```
375    /// Convert this stop into a boxed trait object.
376    ///
377    /// **Prefer [`into_token()`](StopExt::into_token)** which returns a [`StopToken`]
378    /// that is `Clone` and supports indirection collapsing.
379    #[cfg(feature = "alloc")]
380    #[inline]
381    fn into_boxed(self) -> BoxedStop
382    where
383        Self: 'static,
384    {
385        BoxedStop::new(self)
386    }
387
388    /// Convert this stop into a shared, cloneable [`StopToken`].
389    ///
390    /// The result is `Clone` (via `Arc` increment) and can be sent across
391    /// threads. Use this when you need owned, cloneable type erasure.
392    ///
393    /// If `self` is already a `StopToken`, this is a no-op (no double-wrapping).
394    ///
395    /// # Example
396    ///
397    /// ```rust
398    /// # #[cfg(feature = "alloc")]
399    /// # fn main() {
400    /// use almost_enough::{Stopper, StopToken, Stop, StopExt};
401    ///
402    /// let stop = Stopper::new();
403    /// let dyn_stop: StopToken = stop.into_token();
404    /// let clone = dyn_stop.clone(); // cheap Arc increment
405    /// # }
406    /// # #[cfg(not(feature = "alloc"))]
407    /// # fn main() {}
408    /// ```
409    #[cfg(feature = "alloc")]
410    #[inline]
411    fn into_token(self) -> StopToken
412    where
413        Self: 'static,
414    {
415        StopToken::new(self)
416    }
417
418    /// Create a child stop that inherits cancellation from this stop.
419    ///
420    /// The returned [`ChildStopper`] will stop if:
421    /// - Its own `cancel()` is called
422    /// - This parent stop is cancelled
423    ///
424    /// Cancelling the child does NOT affect the parent.
425    ///
426    /// # Example
427    ///
428    /// ```rust
429    /// # #[cfg(feature = "alloc")]
430    /// # fn main() {
431    /// use almost_enough::{Stopper, Stop, StopExt};
432    ///
433    /// let parent = Stopper::new();
434    /// let child = parent.child();
435    ///
436    /// // Child cancellation is independent
437    /// child.cancel();
438    /// assert!(!parent.should_stop());
439    /// assert!(child.should_stop());
440    ///
441    /// // Parent cancellation propagates
442    /// let child2 = parent.child();
443    /// parent.cancel();
444    /// assert!(child2.should_stop());
445    /// # }
446    /// # #[cfg(not(feature = "alloc"))]
447    /// # fn main() {}
448    /// ```
449    #[cfg(feature = "alloc")]
450    #[inline]
451    fn child(&self) -> ChildStopper
452    where
453        Self: Clone + 'static,
454    {
455        ChildStopper::with_parent(self.clone())
456    }
457}
458
459// Blanket implementation for all Stop + Sized types
460impl<T: Stop + Sized> StopExt for T {}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn or_extension_works() {
468        let a = StopSource::new();
469        let b = StopSource::new();
470        let combined = a.as_ref().or(b.as_ref());
471
472        assert!(!combined.should_stop());
473
474        a.cancel();
475        assert!(combined.should_stop());
476    }
477
478    #[test]
479    fn or_chain_works() {
480        let a = StopSource::new();
481        let b = StopSource::new();
482        let c = StopSource::new();
483
484        let combined = a.as_ref().or(b.as_ref()).or(c.as_ref());
485
486        assert!(!combined.should_stop());
487
488        c.cancel();
489        assert!(combined.should_stop());
490    }
491
492    #[test]
493    fn or_with_unstoppable() {
494        let source = StopSource::new();
495        let combined = Unstoppable.or(source.as_ref());
496
497        assert!(!combined.should_stop());
498
499        source.cancel();
500        assert!(combined.should_stop());
501    }
502
503    #[test]
504    fn reexports_work() {
505        // Verify that re-exports from enough work
506        let _: StopReason = StopReason::Cancelled;
507        let _ = Unstoppable;
508        let source = StopSource::new();
509        let _ = source.as_ref();
510    }
511
512    #[cfg(feature = "alloc")]
513    #[test]
514    fn alloc_reexports_work() {
515        let stop = Stopper::new();
516        let _ = stop.clone();
517        let _ = BoxedStop::new(Unstoppable);
518    }
519
520    #[cfg(feature = "alloc")]
521    #[test]
522    fn into_boxed_works() {
523        let stop = Stopper::new();
524        let boxed: BoxedStop = stop.clone().into_boxed();
525
526        assert!(!boxed.should_stop());
527
528        stop.cancel();
529        assert!(boxed.should_stop());
530    }
531
532    #[cfg(feature = "alloc")]
533    #[test]
534    fn into_boxed_with_unstoppable() {
535        let boxed: BoxedStop = Unstoppable.into_boxed();
536        assert!(!boxed.should_stop());
537    }
538
539    #[cfg(feature = "alloc")]
540    #[test]
541    fn into_boxed_prevents_monomorphization() {
542        // This test verifies the pattern compiles correctly
543        fn outer(stop: impl Stop + 'static) {
544            inner(stop.into_boxed());
545        }
546
547        fn inner(stop: BoxedStop) {
548            let _ = stop.should_stop();
549        }
550
551        let stop = Stopper::new();
552        outer(stop);
553        outer(Unstoppable);
554    }
555
556    #[cfg(feature = "alloc")]
557    #[test]
558    fn child_extension_works() {
559        let parent = Stopper::new();
560        let child = parent.child();
561
562        assert!(!child.should_stop());
563
564        parent.cancel();
565        assert!(child.should_stop());
566    }
567
568    #[cfg(feature = "alloc")]
569    #[test]
570    fn child_independent_cancel() {
571        let parent = Stopper::new();
572        let child = parent.child();
573
574        child.cancel();
575
576        assert!(child.should_stop());
577        assert!(!parent.should_stop());
578    }
579
580    #[cfg(feature = "alloc")]
581    #[test]
582    fn child_chain() {
583        let grandparent = Stopper::new();
584        let parent = grandparent.child();
585        let child = parent.child();
586
587        grandparent.cancel();
588
589        assert!(parent.should_stop());
590        assert!(child.should_stop());
591    }
592}