actr_runtime/lifecycle/
actr_system.rs

1//! ActrSystem - Generic-free infrastructure
2
3use actr_config::Config;
4use actr_framework::Workload;
5use actr_protocol::ActorResult;
6use std::sync::Arc;
7
8use super::ActrNode;
9use crate::context_factory::ContextFactory;
10
11// Use types from sub-crates
12use crate::wire::webrtc::{
13    ReconnectConfig, SignalingClient, SignalingConfig, WebSocketSignalingClient,
14};
15use actr_mailbox::{DeadLetterQueue, Mailbox};
16
17/// ActrSystem - Runtime infrastructure (generic-free)
18///
19/// # Design Philosophy
20/// - Phase 1: Create pure runtime framework
21/// - Knows nothing about business logic types
22/// - Transforms into ActrNode<W> via attach()
23pub struct ActrSystem {
24    /// Runtime configuration
25    config: Config,
26
27    /// SQLite persistent mailbox
28    mailbox: Arc<dyn Mailbox>,
29
30    /// Dead Letter Queue for poison messages
31    dlq: Arc<dyn DeadLetterQueue>,
32
33    /// Context factory (with inproc_gate ready, outproc_gate deferred)
34    context_factory: ContextFactory,
35
36    /// Signaling client
37    signaling_client: Arc<dyn SignalingClient>,
38}
39
40impl ActrSystem {
41    /// Create new ActrSystem
42    ///
43    /// # Errors
44    /// - Mailbox initialization failed
45    /// - Transport initialization failed
46    pub async fn new(config: Config) -> ActorResult<Self> {
47        tracing::info!("🚀 Initializing ActrSystem");
48
49        // Initialize Mailbox (using SqliteMailbox implementation)
50        let mailbox_path = config
51            .mailbox_path
52            .as_ref()
53            .map(|p| p.to_string_lossy().to_string())
54            .unwrap_or_else(|| ":memory:".to_string());
55
56        tracing::info!("📂 Mailbox database path: {}", mailbox_path);
57
58        let mailbox: Arc<dyn Mailbox> = Arc::new(
59            actr_mailbox::SqliteMailbox::new(&mailbox_path)
60                .await
61                .map_err(|e| {
62                    actr_protocol::ProtocolError::TransportError(format!(
63                        "Mailbox init failed: {e}"
64                    ))
65                })?,
66        );
67
68        // Initialize Dead Letter Queue
69        // Use same path as mailbox, DLQ will create separate table in same database
70        let dlq_path = if mailbox_path == ":memory:" {
71            ":memory:".to_string() // Separate in-memory DB for DLQ
72        } else {
73            format!("{mailbox_path}.dlq") // Separate file for DLQ
74        };
75
76        let dlq: Arc<dyn DeadLetterQueue> = Arc::new(
77            actr_mailbox::SqliteDeadLetterQueue::new_standalone(&dlq_path)
78                .await
79                .map_err(|e| {
80                    actr_protocol::ProtocolError::TransportError(format!("DLQ init failed: {e}"))
81                })?,
82        );
83        tracing::info!("✅ Dead Letter Queue initialized");
84
85        // Initialize signaling client (using WebSocketSignalingClient implementation)
86        let signaling_config = SignalingConfig {
87            server_url: config.signaling_url.clone(),
88            connection_timeout: 30,
89            heartbeat_interval: 30,
90            reconnect_config: ReconnectConfig::default(),
91            auth_config: None,
92        };
93        let signaling_client: Arc<dyn SignalingClient> =
94            Arc::new(WebSocketSignalingClient::new(signaling_config));
95
96        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97        // Initialize inproc infrastructure (Shell/Local communication - immediately available)
98        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99        use crate::outbound::{InprocOutGate, OutGate};
100        use crate::transport::InprocTransportManager;
101
102        // Create TWO separate InprocTransportManager instances for bidirectional communication
103        // This ensures Shell's pending_requests and Workload's pending_requests are separate
104
105        // Direction 1: Shell → Workload (REQUEST)
106        let shell_to_workload = Arc::new(InprocTransportManager::new());
107        tracing::debug!("✨ Created shell_to_workload InprocTransportManager");
108
109        // Direction 2: Workload → Shell (RESPONSE)
110        let workload_to_shell = Arc::new(InprocTransportManager::new());
111        tracing::debug!("✨ Created workload_to_shell InprocTransportManager");
112
113        // Shell uses shell_to_workload for sending
114        let inproc_gate =
115            OutGate::InprocOut(Arc::new(InprocOutGate::new(shell_to_workload.clone())));
116
117        // Create DataStreamRegistry for DataStream callbacks
118        let data_stream_registry = Arc::new(crate::inbound::DataStreamRegistry::new());
119        tracing::debug!("✨ Created DataStreamRegistry");
120
121        // Create MediaFrameRegistry for MediaTrack callbacks
122        let media_frame_registry = Arc::new(crate::inbound::MediaFrameRegistry::new());
123        tracing::debug!("✨ Created MediaFrameRegistry");
124
125        // ContextFactory holds both managers and registries
126        let context_factory = ContextFactory::new(
127            inproc_gate,
128            shell_to_workload.clone(),
129            workload_to_shell.clone(),
130            data_stream_registry,
131            media_frame_registry,
132            signaling_client.clone(),
133        );
134
135        tracing::info!("✅ Inproc infrastructure initialized (bidirectional Shell ↔ Workload)");
136
137        tracing::info!("✅ ActrSystem initialized");
138
139        Ok(Self {
140            config,
141            mailbox,
142            dlq,
143            context_factory,
144            signaling_client,
145        })
146    }
147
148    /// Attach Workload, transform into ActrNode<W>
149    ///
150    /// # Type Inference
151    /// - Infer W::Dispatcher from W
152    /// - Compiler monomorphizes ActrNode<W>
153    /// - Completely zero-dyn, full inline chain
154    ///
155    /// # Consumes self
156    /// - Move ensures can only be called once
157    /// - Embodies one-actor-per-instance principle
158    pub fn attach<W: Workload>(self, workload: W) -> ActrNode<W> {
159        tracing::info!("📦 Attaching workload");
160
161        ActrNode {
162            config: self.config,
163            workload: Arc::new(workload),
164            mailbox: self.mailbox,
165            dlq: self.dlq,
166            context_factory: Some(self.context_factory), // Initialized with inproc_gate ready
167            signaling_client: self.signaling_client,
168            actor_id: None,              // Obtained after startup
169            credential: None,            // Obtained after startup
170            webrtc_coordinator: None,    // Created after startup
171            webrtc_gate: None,           // Created after startup
172            inproc_mgr: None,            // Set after startup
173            workload_to_shell_mgr: None, // Set after startup
174            shutdown_token: tokio_util::sync::CancellationToken::new(),
175        }
176    }
177}