actr_framework/dest.rs
1//! Destination identifier for Actor communication
2//!
3//! Defines the `Dest` enum with three-way destination distinction:
4//! - **Shell**: Workload → App (inproc reverse channel)
5//! - **Local**: Target local Workload (inproc from App, outproc short-circuit from Workload)
6//! - **Actor**: Remote Actor (full outproc)
7//!
8//! # Design Rationale
9//!
10//! The three-way distinction provides:
11//! - **Clear semantics**: Shell/Local/Actor have distinct meanings
12//! - **Symmetric communication**: App ↔ Workload bidirectional calls
13//! - **Protocol consistency**: Workload self-calls use full serialization (same as remote)
14//! - **Transparent optimization**: Transport layer can short-circuit Local calls
15//!
16//! # Usage
17//!
18//! **Shell 侧 (App)**:
19//! ```rust,ignore
20//! // Call local Workload (隐含 Dest::Local)
21//! running_node.call(request).await?; // No target parameter
22//! ```
23//!
24//! **Actr 侧 (Workload)**:
25//! ```rust,ignore
26//! // Call App
27//! ctx.call(&Dest::Shell, request).await?;
28//!
29//! // Call self (outproc short-circuit)
30//! ctx.call(&Dest::Local, request).await?;
31//!
32//! // Call remote Actor
33//! ctx.call(&Dest::Actor(server_id), request).await?;
34//! ```
35//!
36//! # Placement in actr-framework
37//!
38//! `Dest` is placed in `actr-framework` (not `actr-protocol`) because:
39//! - It's an API-level abstraction, not a protocol data type
40//! - It's used directly by the `Context` trait
41//! - `RpcEnvelope` (in protocol layer) does not contain destination information
42//! - The runtime layer implements the routing logic based on `Dest`
43
44use actr_protocol::ActrId;
45use std::hash::{Hash, Hasher};
46
47/// Destination identifier
48///
49/// Three-way destination for message routing.
50///
51/// # Semantics
52///
53/// - **`Dest::Shell`**: Workload → App (inproc 反向通道)
54/// - Used by Workload to call App side
55/// - Routed through `InprocOutGate` (zero serialization)
56/// - Example: Workload pushing notifications to App
57///
58/// - **`Dest::Local`**: Target local Workload
59/// - From App: routed through `InprocOutGate` (zero serialization)
60/// - From Workload: routed through `OutprocOutGate` (full serialization, short-circuit at transport)
61/// - Example: App calling its local Workload, or Workload calling itself
62///
63/// - **`Dest::Actor(ActrId)`**: Remote Actor (full outproc)
64/// - Used for cross-process Actor communication
65/// - Routed through `OutprocOutGate` (WebRTC/WebSocket)
66/// - Example: ClientWorkload calling RemoteServer
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum Dest {
69 /// Local Shell - Workload 调用 App 侧 (inproc 反向通道)
70 Shell,
71
72 /// Local Workload - 调用本地 Workload (从 App: inproc, 从 Workload: outproc 短接)
73 Local,
74
75 /// Remote Actor - 跨进程通信 (WebRTC/WebSocket)
76 Actor(ActrId),
77}
78
79impl Hash for Dest {
80 #[inline]
81 fn hash<H: Hasher>(&self, state: &mut H) {
82 match self {
83 Dest::Shell => {
84 0u8.hash(state);
85 }
86 Dest::Local => {
87 1u8.hash(state);
88 }
89 Dest::Actor(id) => {
90 2u8.hash(state);
91 id.hash(state);
92 }
93 }
94 }
95}
96
97impl Dest {
98 /// Create Shell destination
99 #[inline]
100 pub fn shell() -> Self {
101 Dest::Shell
102 }
103
104 /// Create Local destination
105 #[inline]
106 pub fn local() -> Self {
107 Dest::Local
108 }
109
110 /// Create Actor destination
111 #[inline]
112 pub fn actor(id: ActrId) -> Self {
113 Dest::Actor(id)
114 }
115
116 /// Check if this is a Shell destination
117 #[inline]
118 pub fn is_shell(&self) -> bool {
119 matches!(self, Dest::Shell)
120 }
121
122 /// Check if this is a Local destination
123 #[inline]
124 pub fn is_local(&self) -> bool {
125 matches!(self, Dest::Local)
126 }
127
128 /// Check if this is an Actor destination
129 #[inline]
130 pub fn is_actor(&self) -> bool {
131 matches!(self, Dest::Actor(_))
132 }
133
134 /// Get ActrId (if this is an Actor destination)
135 ///
136 /// Returns `None` for `Dest::Shell` or `Dest::Local`.
137 #[inline]
138 pub fn as_actor_id(&self) -> Option<&ActrId> {
139 match self {
140 Dest::Actor(id) => Some(id),
141 _ => None,
142 }
143 }
144}
145
146impl From<ActrId> for Dest {
147 #[inline]
148 fn from(id: ActrId) -> Self {
149 Dest::Actor(id)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_dest_creation() {
159 let shell_dest = Dest::shell();
160 assert!(shell_dest.is_shell());
161 assert!(!shell_dest.is_local());
162 assert!(!shell_dest.is_actor());
163
164 let local_dest = Dest::local();
165 assert!(!local_dest.is_shell());
166 assert!(local_dest.is_local());
167 assert!(!local_dest.is_actor());
168
169 let id = ActrId::default();
170 let actor_dest = Dest::actor(id);
171 assert!(!actor_dest.is_shell());
172 assert!(!actor_dest.is_local());
173 assert!(actor_dest.is_actor());
174 }
175
176 #[test]
177 fn test_dest_hash() {
178 use std::collections::HashMap;
179
180 let id1 = ActrId::default();
181 let mut id2 = ActrId::default();
182 id2.serial_number = 1; // Ensure different ID
183
184 let mut map = HashMap::new();
185 map.insert(Dest::shell(), "shell");
186 map.insert(Dest::local(), "local");
187 map.insert(Dest::actor(id1), "actor1");
188 map.insert(Dest::actor(id2), "actor2");
189
190 assert_eq!(map.len(), 4);
191 }
192
193 #[test]
194 fn test_dest_as_actor_id() {
195 let shell_dest = Dest::shell();
196 assert_eq!(shell_dest.as_actor_id(), None);
197
198 let local_dest = Dest::local();
199 assert_eq!(local_dest.as_actor_id(), None);
200
201 let id = ActrId::default();
202 let actor_dest = Dest::actor(id.clone());
203 assert_eq!(actor_dest.as_actor_id(), Some(&id));
204 }
205}