use std::future::Future;
use std::sync::{Arc, Mutex};
use crate::{NativeId, SourceError};
tokio::task_local! {
static HELD: Arc<HeldVersion>;
}
#[derive(Debug)]
pub struct HeldVersion {
id: NativeId,
version: String,
moved: Mutex<Option<String>>,
}
impl HeldVersion {
#[must_use]
pub fn new(id: NativeId, version: String) -> Self {
Self {
id,
version,
moved: Mutex::new(None),
}
}
pub async fn scope<F: Future>(held: Arc<Self>, call: F) -> F::Output {
HELD.scope(held, call).await
}
#[must_use]
pub fn expected(id: &NativeId) -> Option<String> {
HELD.try_with(|held| (&held.id == id).then(|| held.version.clone()))
.ok()
.flatten()
}
#[must_use]
pub fn moved(id: &NativeId, found: Option<String>) -> SourceError {
let found = found.unwrap_or_default();
let _ = HELD.try_with(|held| {
if &held.id == id {
*held
.moved
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(found.clone());
}
});
SourceError::Refused {
message: format!(
"task {id} is at version {found:?}, not the version the write is held to, so \
nothing was written"
),
}
}
#[must_use]
pub fn found(&self) -> Option<String> {
self.moved
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
#[must_use]
pub const fn id(&self) -> &NativeId {
&self.id
}
#[must_use]
pub fn version(&self) -> &str {
&self.version
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_hold_answers_only_inside_its_scope_and_only_for_its_task() {
let id = NativeId::from("t1");
assert_eq!(HeldVersion::expected(&id), None);
let held = Arc::new(HeldVersion::new(id.clone(), "v1".to_owned()));
let inside = HeldVersion::scope(Arc::clone(&held), async {
let other = HeldVersion::expected(&NativeId::from("t2"));
let refused = HeldVersion::moved(&id, Some("v2".to_owned()));
(HeldVersion::expected(&id), other, refused)
})
.await;
assert_eq!(inside.0.as_deref(), Some("v1"));
assert_eq!(inside.1, None);
assert!(matches!(inside.2, SourceError::Refused { .. }));
assert_eq!(held.found().as_deref(), Some("v2"));
}
}