little_durable_objects/actor/
protocol.rs1use 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#[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#[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 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 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}