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