Skip to main content

little_durable_objects/actor/
protocol.rs

1use anyhow::{Result, ensure};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use crate::actor_state::ActorStorageKey;
6
7const MAX_NAMESPACE_ID_BYTES: usize = 96;
8const MAX_ACTOR_TYPE_BYTES: usize = 48;
9const MAX_ACTOR_ID_BYTES: usize = 128;
10const MAX_METHOD_BYTES: usize = 128;
11
12/// The authenticated tenant boundary shared by a collection of actors.
13#[derive(Clone, Debug)]
14pub struct ActorScope {
15    pub namespace_id: String,
16}
17
18impl ActorScope {
19    pub fn validate(&self) -> Result<()> {
20        validate_component("namespace ID", &self.namespace_id, MAX_NAMESPACE_ID_BYTES)
21    }
22
23    pub fn contains(&self, actor: &ActorKey) -> bool {
24        actor.namespace_id == self.namespace_id
25    }
26}
27
28/// The namespace-scoped identity of one actor instance.
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub struct ActorKey {
31    pub namespace_id: String,
32    pub actor_type: String,
33    pub actor_id: String,
34}
35
36impl ActorKey {
37    pub fn validate(&self) -> Result<()> {
38        validate_component("namespace ID", &self.namespace_id, MAX_NAMESPACE_ID_BYTES)?;
39        validate_component("actor type", &self.actor_type, MAX_ACTOR_TYPE_BYTES)?;
40        validate_component("actor ID", &self.actor_id, MAX_ACTOR_ID_BYTES)?;
41        self.storage_key().validate()
42    }
43
44    /// Stable, readable identity used for coordination records.
45    pub fn storage_key(&self) -> ActorStorageKey {
46        ActorStorageKey::new(format!(
47            "object.v1.{}.{}.{}",
48            self.namespace_id, self.actor_type, self.actor_id
49        ))
50    }
51}
52
53#[derive(Clone, Debug, PartialEq)]
54pub struct ActorInvocation {
55    /// Correlation ID for this caller attempt. It is not an idempotency key.
56    pub request_id: String,
57    pub actor: ActorKey,
58    pub method: String,
59    pub args: Vec<Value>,
60}
61
62impl ActorInvocation {
63    pub fn validate(&self) -> Result<()> {
64        ensure!(!self.request_id.is_empty(), "request ID must not be empty");
65        ensure!(
66            self.request_id.len() <= 255,
67            "request ID must be at most 255 bytes"
68        );
69        self.actor.validate()?;
70        validate_component("actor method", &self.method, MAX_METHOD_BYTES)?;
71        Ok(())
72    }
73}
74
75#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76pub struct ActorInvocationFailure {
77    pub code: String,
78    pub message: String,
79}
80
81impl ActorInvocationFailure {
82    pub(crate) fn outcome_unknown_after_execution() -> Self {
83        Self {
84            code: "outcome_unknown".into(),
85            message: "actor execution completed, but its durable publication outcome could not be confirmed".into(),
86        }
87    }
88}
89
90#[derive(Clone, Debug, PartialEq)]
91pub enum ActorExecutionResult {
92    Completed { result: Value },
93    Failed { failure: ActorInvocationFailure },
94    Reroute,
95    HostUnavailable,
96}
97
98fn validate_component(name: &str, value: &str, max_bytes: usize) -> Result<()> {
99    ensure!(!value.is_empty(), "{name} must not be empty");
100    ensure!(
101        value.len() <= max_bytes,
102        "{name} must be at most {max_bytes} bytes"
103    );
104    ensure!(
105        value
106            .bytes()
107            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')),
108        "{name} may contain only ASCII letters, digits, '.', '-', and '_'"
109    );
110    Ok(())
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn maps_a_tenant_scoped_actor_to_one_safe_object_id() {
119        let key = ActorKey {
120            namespace_id: "namespace-1".into(),
121            actor_type: "counter".into(),
122            actor_id: "customer.123".into(),
123        };
124
125        key.validate().expect("actor key");
126        assert_eq!(
127            key.storage_key().as_str(),
128            "object.v1.namespace-1.counter.customer.123"
129        );
130    }
131
132    #[test]
133    fn rejects_components_that_can_reshape_storage_paths() {
134        let mut key = ActorKey {
135            namespace_id: "namespace-1".into(),
136            actor_type: "counter".into(),
137            actor_id: "../other".into(),
138        };
139        assert!(key.validate().is_err());
140
141        key.actor_id = "valid".into();
142        key.actor_type = "counter/type".into();
143        assert!(key.validate().is_err());
144    }
145}