actr_framework/dest.rs
1//! Destination identifier for Actor communication
2//!
3//! Defines the `Dest` enum with three-way destination distinction:
4//! - **Host**: this node's host side (the process embedding the workload)
5//! - **Workload**: this node's workload side (self when sent from the workload)
6//! - **Peer**: a remote node, addressed by `ActrId`
7//!
8//! # Design Rationale
9//!
10//! All three variants answer the same question — *which role receives the
11//! message* — relative to the sending node:
12//! - **Consistent axis**: Host / Workload / Peer are role nouns; `Peer` makes
13//! "crosses the network" visible in the name
14//! - **Symmetric communication**: Host ↔ Workload bidirectional calls
15//! - **Protocol consistency**: Workload self-calls use full serialization (same as remote)
16//! - **Transparent optimization**: Transport layer can short-circuit Workload calls
17//!
18//! # Usage
19//!
20//! **Host side (embedding process)**:
21//! ```rust,ignore
22//! // Call local Workload (implies Dest::Workload)
23//! running_node.call(request).await?; // No target parameter
24//! ```
25//!
26//! **Workload side**:
27//! ```rust,ignore
28//! // Call the host side
29//! ctx.call(&Dest::Host, request).await?;
30//!
31//! // Call self (outproc short-circuit)
32//! ctx.call(&Dest::Workload, request).await?;
33//!
34//! // Call a remote peer
35//! ctx.call(&Dest::Peer(server_id), request).await?;
36//! ```
37//!
38//! # Placement in actr-framework
39//!
40//! `Dest` is placed in `actr-framework` (not `actr-protocol`) because:
41//! - It's an API-level abstraction, not a protocol data type
42//! - It's used directly by the `Context` trait
43//! - `RpcEnvelope` (in protocol layer) does not contain destination information
44//! - The runtime layer implements the routing logic based on `Dest`
45
46use actr_protocol::ActrId;
47
48/// Destination identifier
49///
50/// Three-way destination for message routing.
51///
52/// # Semantics
53///
54/// - **`Dest::Host`**: Workload -> host side (inproc reverse channel)
55/// - Used by the Workload to call the process embedding it
56/// - Routed through `HostGate` (zero serialization)
57/// - Example: Workload pushing notifications to the host application
58///
59/// - **`Dest::Workload`**: Target this node's Workload
60/// - From the host side: routed through `HostGate` (zero serialization)
61/// - From the Workload: routed through `PeerGate` (full serialization, short-circuit at transport)
62/// - Example: host calling its local Workload, or Workload calling itself
63///
64/// - **`Dest::Peer(ActrId)`**: Remote node (full outproc)
65/// - Used for cross-process Actor communication
66/// - Routed through `PeerGate` (WebRTC/WebSocket)
67/// - Example: ClientWorkload calling RemoteServer
68#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69pub enum Dest {
70 /// This node's host side - Workload calls the embedding process (inproc reverse channel)
71 Host,
72
73 /// This node's Workload - self when sent from the workload (from host: inproc, from Workload: outproc short-circuit)
74 Workload,
75
76 /// Remote node - cross-process communication (WebRTC/WebSocket)
77 Peer(ActrId),
78}
79
80impl Dest {
81 /// Create a Host destination
82 #[inline]
83 pub fn host() -> Self {
84 Dest::Host
85 }
86
87 /// Create a Workload destination
88 #[inline]
89 pub fn workload() -> Self {
90 Dest::Workload
91 }
92
93 /// Create a Peer destination
94 #[inline]
95 pub fn peer(id: ActrId) -> Self {
96 Dest::Peer(id)
97 }
98
99 /// Check if this is a Host destination
100 #[inline]
101 pub fn is_host(&self) -> bool {
102 matches!(self, Dest::Host)
103 }
104
105 /// Check if this is a Workload destination
106 #[inline]
107 pub fn is_workload(&self) -> bool {
108 matches!(self, Dest::Workload)
109 }
110
111 /// Check if this is a Peer destination
112 #[inline]
113 pub fn is_peer(&self) -> bool {
114 matches!(self, Dest::Peer(_))
115 }
116
117 /// Get the peer's ActrId if this is a Peer destination
118 #[inline]
119 pub fn as_peer_id(&self) -> Option<&ActrId> {
120 match self {
121 Dest::Peer(id) => Some(id),
122 _ => None,
123 }
124 }
125}
126
127impl From<ActrId> for Dest {
128 #[inline]
129 fn from(id: ActrId) -> Self {
130 Dest::Peer(id)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_dest_creation() {
140 let host_dest = Dest::host();
141 assert!(host_dest.is_host());
142 assert!(!host_dest.is_workload());
143 assert!(!host_dest.is_peer());
144
145 let workload_dest = Dest::workload();
146 assert!(!workload_dest.is_host());
147 assert!(workload_dest.is_workload());
148 assert!(!workload_dest.is_peer());
149
150 let id = ActrId::default();
151 let peer_dest = Dest::peer(id);
152 assert!(!peer_dest.is_host());
153 assert!(!peer_dest.is_workload());
154 assert!(peer_dest.is_peer());
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::host(), "host");
169 map.insert(Dest::workload(), "workload");
170 map.insert(Dest::peer(id1), "peer1");
171 map.insert(Dest::peer(id2), "peer2");
172
173 assert_eq!(map.len(), 4);
174 }
175
176 #[test]
177 fn test_dest_as_peer_id() {
178 let host_dest = Dest::host();
179 assert_eq!(host_dest.as_peer_id(), None);
180
181 let workload_dest = Dest::workload();
182 assert_eq!(workload_dest.as_peer_id(), None);
183
184 let id = ActrId {
185 serial_number: 7,
186 ..Default::default()
187 };
188 let peer_dest = Dest::peer(id.clone());
189 assert_eq!(peer_dest.as_peer_id(), Some(&id));
190 }
191
192 #[test]
193 fn test_dest_from_actr_id() {
194 let id = ActrId::default();
195 let dest: Dest = id.clone().into();
196 assert_eq!(dest, Dest::Peer(id));
197 }
198}