use std::sync::Arc;
use tokio::sync::watch;
#[derive(Debug, thiserror::Error)]
pub enum CancellationError {
#[error("Cancellation channel closed")]
ChannelClosed,
}
#[derive(Clone)]
pub struct CancellationToken {
receiver: watch::Receiver<bool>,
}
pub struct CancellationTokenSource {
sender: Arc<watch::Sender<bool>>,
}
impl CancellationTokenSource {
pub fn new() -> (Self, CancellationToken) {
let (sender, receiver) = watch::channel(false);
(
CancellationTokenSource {
sender: Arc::new(sender),
},
CancellationToken { receiver },
)
}
pub fn cancel(&self) -> Result<(), CancellationError> {
self.sender
.send(true)
.map_err(|_| CancellationError::ChannelClosed)
}
#[allow(unused)]
pub fn token(&self) -> CancellationToken {
CancellationToken {
receiver: self.sender.subscribe(),
}
}
}
impl CancellationToken {
#[cfg(any(feature = "sse", feature = "streamable-http"))]
pub fn is_cancelled(&self) -> bool {
*self.receiver.borrow()
}
pub async fn cancelled(&self) -> Result<(), CancellationError> {
let mut receiver = self.receiver.clone();
loop {
if *receiver.borrow() {
return Ok(());
}
receiver
.changed()
.await
.map_err(|_| CancellationError::ChannelClosed)?;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::{timeout, Duration};
#[tokio::test]
async fn test_create_and_initial_state() {
let (_source, token) = CancellationTokenSource::new();
assert!(!token.is_cancelled());
let wait_result = timeout(Duration::from_millis(100), token.cancelled()).await;
assert!(
wait_result.is_err(),
"Expected timeout as cancellation not triggered"
);
}
#[tokio::test]
async fn test_trigger_cancellation() {
let (source, token) = CancellationTokenSource::new();
let cancel_result = source.cancel();
assert!(cancel_result.is_ok(), "Expected successful cancellation");
assert!(token.is_cancelled());
let wait_result = timeout(Duration::from_millis(100), token.cancelled()).await;
assert!(wait_result.is_ok(), "Expected cancellation to complete");
assert!(
wait_result.unwrap().is_ok(),
"Expected Ok result from cancelled()"
);
}
#[tokio::test]
async fn test_await_cancellation() {
let (source, token) = CancellationTokenSource::new();
let source_clone = source;
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
let _ = source_clone.cancel();
});
let wait_result = timeout(Duration::from_millis(200), token.cancelled()).await;
assert!(wait_result.is_ok(), "Expected cancellation within timeout");
assert!(
wait_result.unwrap().is_ok(),
"Expected Ok result from cancelled()"
);
assert!(token.is_cancelled(), "Expected token to be cancelled");
}
#[tokio::test]
async fn test_multiple_tokens() {
let (source, token1) = CancellationTokenSource::new();
let token2 = source.token();
let token3 = source.token();
assert!(!token1.is_cancelled());
assert!(!token2.is_cancelled());
assert!(!token3.is_cancelled());
source.cancel().expect("Failed to cancel");
assert!(token1.is_cancelled());
assert!(token2.is_cancelled());
assert!(token3.is_cancelled());
let wait1 = timeout(Duration::from_millis(100), token1.cancelled()).await;
let wait2 = timeout(Duration::from_millis(100), token2.cancelled()).await;
let wait3 = timeout(Duration::from_millis(100), token3.cancelled()).await;
assert!(
wait1.is_ok() && wait1.unwrap().is_ok(),
"Token1 should complete cancellation"
);
assert!(
wait2.is_ok() && wait2.unwrap().is_ok(),
"Token2 should complete cancellation"
);
assert!(
wait3.is_ok() && wait3.unwrap().is_ok(),
"Token3 should complete cancellation"
);
}
#[tokio::test]
async fn test_channel_closed_error() {
let (source, token) = CancellationTokenSource::new();
drop(source);
let wait_result = token.cancelled().await;
assert!(
matches!(wait_result, Err(CancellationError::ChannelClosed)),
"Expected ChannelClosed error"
);
assert!(!token.is_cancelled());
}
#[tokio::test]
async fn test_new_token_creation() {
let (source, token1) = CancellationTokenSource::new();
let token2 = source.token();
assert!(!token1.is_cancelled());
assert!(!token2.is_cancelled());
source.cancel().expect("Failed to cancel");
assert!(token1.is_cancelled());
assert!(token2.is_cancelled());
let wait1 = timeout(Duration::from_millis(100), token1.cancelled()).await;
let wait2 = timeout(Duration::from_millis(100), token2.cancelled()).await;
assert!(
wait1.is_ok() && wait1.unwrap().is_ok(),
"Token1 should complete cancellation"
);
assert!(
wait2.is_ok() && wait2.unwrap().is_ok(),
"Token2 should complete cancellation"
);
}
}