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 side (App)**:
19//! ```rust,ignore
20//! // Call local Workload (implies Dest::Local)
21//! running_node.call(request).await?; // No target parameter
22//! ```
23//!
24//! **Actr side (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;
45
46/// Destination identifier
47///
48/// Three-way destination for message routing.
49///
50/// # Semantics
51///
52/// - **`Dest::Shell`**: Workload -> App (inproc reverse channel)
53/// - Used by Workload to call App side
54/// - Routed through `HostGate` (zero serialization)
55/// - Example: Workload pushing notifications to App
56///
57/// - **`Dest::Local`**: Target local Workload
58/// - From App: routed through `HostGate` (zero serialization)
59/// - From Workload: routed through `PeerGate` (full serialization, short-circuit at transport)
60/// - Example: App calling its local Workload, or Workload calling itself
61///
62/// - **`Dest::Actor(ActrId)`**: Remote Actor (full outproc)
63/// - Used for cross-process Actor communication
64/// - Routed through `PeerGate` (WebRTC/WebSocket)
65/// - Example: ClientWorkload calling RemoteServer
66#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub enum Dest {
68 /// Local Shell - Workload calls App side (inproc reverse channel)
69 Shell,
70
71 /// Local Workload - calls local Workload (from App: inproc, from Workload: outproc short-circuit)
72 Local,
73
74 /// Remote Actor - cross-process communication (WebRTC/WebSocket)
75 Actor(ActrId),
76}
77
78impl Dest {
79 /// Create Shell destination
80 #[inline]
81 pub fn shell() -> Self {
82 Dest::Shell
83 }
84
85 /// Create Local destination
86 #[inline]
87 pub fn local() -> Self {
88 Dest::Local
89 }
90
91 /// Create Actor destination
92 #[inline]
93 pub fn actor(id: ActrId) -> Self {
94 Dest::Actor(id)
95 }
96
97 /// Check if this is a Shell destination
98 #[inline]
99 pub fn is_shell(&self) -> bool {
100 matches!(self, Dest::Shell)
101 }
102
103 /// Check if this is a Local destination
104 #[inline]
105 pub fn is_local(&self) -> bool {
106 matches!(self, Dest::Local)
107 }
108
109 /// Check if this is an Actor destination
110 #[inline]
111 pub fn is_actor(&self) -> bool {
112 matches!(self, Dest::Actor(_))
113 }
114
115 /// Get ActrId (if this is an Actor destination)
116 ///
117 /// Returns `None` for `Dest::Shell` or `Dest::Local`.
118 #[inline]
119 pub fn as_actor_id(&self) -> Option<&ActrId> {
120 match self {
121 Dest::Actor(id) => Some(id),
122 _ => None,
123 }
124 }
125}
126
127impl From<ActrId> for Dest {
128 #[inline]
129 fn from(id: ActrId) -> Self {
130 Dest::Actor(id)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_dest_creation() {
140 let shell_dest = Dest::shell();
141 assert!(shell_dest.is_shell());
142 assert!(!shell_dest.is_local());
143 assert!(!shell_dest.is_actor());
144
145 let local_dest = Dest::local();
146 assert!(!local_dest.is_shell());
147 assert!(local_dest.is_local());
148 assert!(!local_dest.is_actor());
149
150 let id = ActrId::default();
151 let actor_dest = Dest::actor(id);
152 assert!(!actor_dest.is_shell());
153 assert!(!actor_dest.is_local());
154 assert!(actor_dest.is_actor());
155 }
156
157 #[test]
158 fn test_dest_hash() {
159 use std::collections::HashMap;
160
161 let id1 = ActrId::default();
162 let id2 = ActrId {
163 serial_number: 1,
164 ..Default::default()
165 };
166
167 let mut map = HashMap::new();
168 map.insert(Dest::shell(), "shell");
169 map.insert(Dest::local(), "local");
170 map.insert(Dest::actor(id1), "actor1");
171 map.insert(Dest::actor(id2), "actor2");
172
173 assert_eq!(map.len(), 4);
174 }
175
176 #[test]
177 fn test_dest_as_actor_id() {
178 let shell_dest = Dest::shell();
179 assert_eq!(shell_dest.as_actor_id(), None);
180
181 let local_dest = Dest::local();
182 assert_eq!(local_dest.as_actor_id(), None);
183
184 let id = ActrId::default();
185 let actor_dest = Dest::actor(id.clone());
186 assert_eq!(actor_dest.as_actor_id(), Some(&id));
187 }
188}