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::ActorSocketEffect, 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, Hash, 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 {
93        result: Value,
94        effects: Vec<ActorSocketEffect>,
95    },
96    Failed {
97        failure: ActorInvocationFailure,
98    },
99    Reroute,
100    HostUnavailable,
101}
102
103fn validate_component(name: &str, value: &str, max_bytes: usize) -> Result<()> {
104    ensure!(!value.is_empty(), "{name} must not be empty");
105    ensure!(
106        value.len() <= max_bytes,
107        "{name} must be at most {max_bytes} bytes"
108    );
109    ensure!(
110        value
111            .bytes()
112            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')),
113        "{name} may contain only ASCII letters, digits, '.', '-', and '_'"
114    );
115    Ok(())
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn maps_a_tenant_scoped_actor_to_one_safe_object_id() {
124        let key = ActorKey {
125            namespace_id: "namespace-1".into(),
126            actor_type: "counter".into(),
127            actor_id: "customer.123".into(),
128        };
129
130        key.validate().expect("actor key");
131        assert_eq!(
132            key.storage_key().as_str(),
133            "object.v1.namespace-1.counter.customer.123"
134        );
135    }
136
137    #[test]
138    fn rejects_components_that_can_reshape_storage_paths() {
139        let mut key = ActorKey {
140            namespace_id: "namespace-1".into(),
141            actor_type: "counter".into(),
142            actor_id: "../other".into(),
143        };
144        assert!(key.validate().is_err());
145
146        key.actor_id = "valid".into();
147        key.actor_type = "counter/type".into();
148        assert!(key.validate().is_err());
149    }
150}