almost-enough 0.4.2

Batteries-included ergonomic extensions for the `enough` cooperative cancellation crate
Documentation

almost-enough

Batteries-included ergonomic extensions for the enough cooperative cancellation crate.

CI Crates.io Documentation codecov License MSRV

While enough provides only the minimal Stop trait, this crate provides all concrete implementations, combinators, and helpers. It re-exports everything from enough for convenience.

Quick Start

use almost_enough::{Stopper, Stop};

let stop = Stopper::new();
let stop2 = stop.clone();  // Clone to share

// Pass to operations
assert!(!stop2.should_stop());

// Any clone can cancel
stop.cancel();
assert!(stop2.should_stop());

Type Overview

Type Feature Use Case
Unstoppable core Zero-cost "never stop"
StopSource / StopRef core Stack-based, borrowed, zero-alloc
FnStop core Wrap any closure
OrStop core Combine multiple stops
Stopper alloc Default choice - Arc-based, clone to share
SyncStopper alloc Like Stopper with Acquire/Release ordering
ChildStopper alloc Hierarchical parent-child cancellation
StopToken alloc Type-erased dynamic dispatch - Arc-based, Clone
BoxedStop alloc Type-erased dynamic dispatch (prefer StopToken)
WithTimeout std Add deadline to any Stop

Features

  • std (default) - Full functionality including timeouts
  • alloc - Arc-based types, into_boxed(), child(), guards
  • None - Core trait and stack-based types only (no_std compatible)

Extension Traits

The StopExt trait adds combinator methods to any Stop:

use almost_enough::{StopSource, Stop, StopExt};

let timeout = StopSource::new();
let cancel = StopSource::new();

// Combine: stop if either stops
let combined = timeout.as_ref().or(cancel.as_ref());
assert!(!combined.should_stop());

cancel.cancel();
assert!(combined.should_stop());

Hierarchical Cancellation

Create child stops that inherit cancellation from their parent:

use almost_enough::{Stopper, Stop, StopExt};

let parent = Stopper::new();
let child = parent.child();

// Child cancellation doesn't affect parent
child.cancel();
assert!(!parent.should_stop());

// But parent cancellation propagates to children
let child2 = parent.child();
parent.cancel();
assert!(child2.should_stop());

Stop Guards (RAII)

Automatically cancel on scope exit unless explicitly disarmed:

use almost_enough::{Stopper, StopDropRoll};

fn do_work(source: &Stopper) -> Result<(), &'static str> {
    let guard = source.stop_on_drop();

    // If we return early or panic, source is stopped
    risky_operation()?;

    // Success! Don't stop.
    guard.disarm();
    Ok(())
}

Type Erasure

Prevent monomorphization explosion at API boundaries with StopToken. Stopper and SyncStopper convert to StopToken at zero cost via Into — the existing Arc is reused, no double-wrapping:

use almost_enough::{CloneStop, StopToken, Stopper, Stop, StopExt};

fn outer(stop: impl CloneStop) {
    // Erase the concrete type — StopToken is Clone (Arc-based)
    // Stopper→StopToken is zero-cost (reuses the same Arc)
    let stop: StopToken = stop.into_token();
    inner(&stop);
}

fn inner(stop: &StopToken) {
    let stop2 = stop.clone(); // cheap Arc increment, no allocation
    // Only one version of this function exists
    while !stop.should_stop() {
        break;
    }
}

Optimizing Hot Loops with dyn Stop

Use may_stop() to skip overhead for no-op stops behind &dyn Stop:

use almost_enough::{Stop, StopReason, Unstoppable};

fn process(stop: &dyn Stop) -> Result<(), StopReason> {
    let stop = stop.may_stop().then_some(stop); // Option<&dyn Stop>
    for i in 0..1_000_000 {
        stop.check()?; // None → Ok(()), Some → one vtable dispatch
    }
    Ok(())
}

// Unstoppable: may_stop() = false, so stop is None — zero overhead
assert!(process(&Unstoppable).is_ok());

StopToken and BoxedStop automatically optimize away no-op stops — when wrapping Unstoppable, check() short-circuits without any vtable dispatch:

use almost_enough::{StopToken, Stopper, Unstoppable, Stop, StopReason};

fn hot_loop(stop: &StopToken) -> Result<(), StopReason> {
    for i in 0..1_000_000 {
        stop.check()?; // Unstoppable: no-op. Stopper: one dispatch.
    }
    Ok(())
}

hot_loop(&StopToken::new(Unstoppable)).unwrap();    // zero overhead
hot_loop(&StopToken::new(Stopper::new())).unwrap(); // one dispatch per check

See Also

  • enough - Minimal core trait (for library authors)
  • enough-tokio - Tokio CancellationToken bridge
  • enough-ffi - FFI helpers for C#, Python, Node.js

License

MIT OR Apache-2.0