Skip to main content

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}