Skip to main content

a3s_code_core/capability/
effect.rs

1use async_trait::async_trait;
2use thiserror::Error;
3
4const MAX_EFFECT_ERROR_BYTES: usize = 1_024;
5
6/// Bounded failure returned by asynchronous capability teardown.
7#[derive(Clone, Debug, Eq, Error, PartialEq)]
8#[error("{message}")]
9pub struct CapabilityEffectError {
10    message: Box<str>,
11}
12
13impl CapabilityEffectError {
14    pub fn new(message: impl Into<String>) -> Self {
15        let message = message.into();
16        let message = if message.is_empty() {
17            "capability effect teardown failed".to_owned()
18        } else {
19            truncate_utf8(message, MAX_EFFECT_ERROR_BYTES)
20        };
21        Self {
22            message: message.into_boxed_str(),
23        }
24    }
25
26    pub fn message(&self) -> &str {
27        &self.message
28    }
29}
30
31/// One reversible resource owned by exactly one capability scope.
32///
33/// Implementations must make `close` idempotent at the underlying resource
34/// boundary. The supervisor calls effects in reverse registration order and
35/// keeps proceeding after an individual failure. Teardown must use asynchronous
36/// I/O and yield normally; blocking a Tokio worker violates the scope close
37/// contract.
38#[async_trait]
39pub trait CapabilityEffect: Send + 'static {
40    fn name(&self) -> &str;
41
42    async fn close(self: Box<Self>) -> Result<(), CapabilityEffectError>;
43}
44
45fn truncate_utf8(mut value: String, max: usize) -> String {
46    if value.len() <= max {
47        return value;
48    }
49    let mut boundary = max;
50    while !value.is_char_boundary(boundary) {
51        boundary -= 1;
52    }
53    value.truncate(boundary);
54    value
55}