actr_framework/context.rs
1//! Context trait - Execution context interface for actors
2
3use actr_protocol::{ActorResult, ActrId, ActrType, DataChunk, PayloadType};
4use async_trait::async_trait;
5use futures_util::future::BoxFuture;
6
7/// Boxed callback future that is `Send` on native targets and local on WASM.
8///
9/// This mirrors [`MaybeSendSync`]: native runtimes may move callbacks between
10/// executor threads, while Component Model and web guests run cooperatively on
11/// one WASM thread and may legitimately capture a non-`Send` [`Context`].
12#[cfg(not(target_arch = "wasm32"))]
13pub type MaybeSendBoxFuture<'a, T> = futures_util::future::BoxFuture<'a, T>;
14
15/// Boxed callback future that is `Send` on native targets and local on WASM.
16#[cfg(target_arch = "wasm32")]
17pub type MaybeSendBoxFuture<'a, T> = futures_util::future::LocalBoxFuture<'a, T>;
18
19// ── MaybeSendSync marker ────────────────────────────────────────────────
20//
21// Auto-trait-style marker that is `Send + Sync` on native targets and empty
22// on `wasm32`. Per Option U γ-unified §3.1 the user-facing `Context` bound
23// is `Clone + 'static`, but native runtime layers (tokio multi-thread,
24// `WorkloadHookObserver`) must produce `Send` futures; pinning `Send + Sync`
25// behind this marker lets the framework trait carry a single `?Send`
26// definition while silently re-asserting the native auto traits through a
27// cfg-gated blanket impl.
28
29/// Auto-trait-style marker — `Send + Sync` on native, empty on `wasm32`.
30///
31/// Used as a supertrait on `Context`, `Workload`, and `MessageDispatcher`
32/// so `async_trait` default bodies compile in both modes without adding
33/// explicit `Send` / `Sync` bounds to the user-visible trait definition.
34#[cfg(not(target_arch = "wasm32"))]
35pub trait MaybeSendSync: Send + Sync {}
36#[cfg(not(target_arch = "wasm32"))]
37impl<T: Send + Sync + ?Sized> MaybeSendSync for T {}
38
39/// Auto-trait-style marker — `Send + Sync` on native, empty on `wasm32`.
40#[cfg(target_arch = "wasm32")]
41pub trait MaybeSendSync {}
42#[cfg(target_arch = "wasm32")]
43impl<T: ?Sized> MaybeSendSync for T {}
44
45/// Actor execution context interface.
46///
47/// Defines the complete interface for an actor to interact with the runtime:
48/// - Context data access (`self_id`, `request_id`, …)
49/// - Communication primitives (`call`, `tell`)
50///
51/// # Design principles
52///
53/// - **Interface only**: the framework provides no implementation; the runtime
54/// does.
55/// - **Generic parameter**: user code accepts `<C: Context>` rather than
56/// `&dyn Context`, so dispatch monomorphises and avoids vtables.
57///
58/// # cfg dispatch
59///
60/// The trait is `?Send` on `wasm32` (browser single-threaded, futures can
61/// legitimately not be `Send`) and `#[async_trait]` (Send mode) on native
62/// targets so tokio-multi-thread spawners downstream keep working. The
63/// `MaybeSendSync` supertrait adds `Send + Sync` on native only — per
64/// Option U γ-unified §3.1 the user-visible bound stays `Clone + 'static`
65/// while `Workload` default method bodies (which need `Send` futures under
66/// the native `async_trait`) compile without any extra constraint on the
67/// generic `C`.
68///
69/// # Example
70///
71/// ```rust,ignore
72/// async fn my_handler<C: Context>(ctx: &C) {
73/// let id = ctx.self_id();
74/// let response = ctx.call(&target, request).await?;
75/// }
76/// ```
77#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
78#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
79pub trait Context: Clone + MaybeSendSync + 'static {
80 // ========== Data Access Methods ==========
81
82 /// Get the current Actor's ID
83 fn self_id(&self) -> &ActrId;
84
85 /// Get the caller's Actor ID
86 ///
87 /// - `Some(caller_id)`: Called by another Actor
88 /// - `None`: System internal call (e.g., lifecycle hooks)
89 fn caller_id(&self) -> Option<&ActrId>;
90
91 /// Get the unique request ID
92 ///
93 /// A new request_id is generated for each RPC call, used to match requests and responses.
94 fn request_id(&self) -> &str;
95
96 // ========== Communication Methods ==========
97
98 /// Send a type-safe RPC request and wait for response
99 ///
100 /// This is the primary way to call other Actors, providing full type safety guarantees.
101 ///
102 /// # Type Inference
103 ///
104 /// Response type is automatically inferred from `R::Response`, no manual annotation needed:
105 ///
106 /// ```rust,ignore
107 /// let request = EchoRequest { message: "hello".to_string() };
108 /// let response: EchoResponse = ctx.call(&target, request).await?;
109 /// // ^^^^^^^^^^^^ Inferred from EchoRequest::Response
110 /// ```
111 ///
112 /// # Error Handling
113 ///
114 /// - `ProtocolError::TransportError`: Network transport failure
115 /// - `ProtocolError::Actr(DecodeFailure)`: Response decode failure
116 /// - `ProtocolError::Actr(UnknownRoute)`: Route does not exist
117 /// - Errors returned by remote Actor's business logic
118 ///
119 /// # Parameters
120 ///
121 /// - `target`: Target destination (`Dest::Host` for local, `Dest::Peer(id)` for remote)
122 /// - `request`: Request message implementing `RpcRequest` trait
123 ///
124 /// # Returns
125 ///
126 /// Returns response message of type `R::Response`
127 async fn call<R: actr_protocol::RpcRequest>(
128 &self,
129 target: &crate::Dest,
130 request: R,
131 ) -> ActorResult<R::Response>;
132
133 /// Send a type-safe one-way message (no response expected)
134 ///
135 /// Used for sending notifications, events, etc. that do not require a response.
136 ///
137 /// # Semantics
138 ///
139 /// - **Fire-and-forget**: Does not wait for response after sending
140 /// - **No delivery guarantee**: Message may be lost if target is unreachable
141 /// - **Low latency**: Does not block waiting for response
142 ///
143 /// # Parameters
144 ///
145 /// - `target`: Target destination (`Dest::Host` for local, `Dest::Peer(id)` for remote)
146 /// - `message`: Message implementing `RpcRequest` trait
147 async fn tell<R: actr_protocol::RpcRequest>(
148 &self,
149 target: &crate::Dest,
150 message: R,
151 ) -> ActorResult<()>;
152
153 // ========== Fast Path: DataChunk Methods ==========
154
155 /// Register a DataChunk callback for a specific stream
156 ///
157 /// When a DataChunk with matching stream_id arrives, the registered callback will be invoked.
158 /// Delivery is FIFO and run-to-completion for one stream. Different streams
159 /// use independent inbound workers, although a serial-only workload backend
160 /// may still serialize their final guest callbacks.
161 ///
162 /// # Parameters
163 ///
164 /// - `stream_id`: Stream identifier (must be globally unique)
165 /// - `callback`: Handler function that receives (DataChunk, sender ActrId)
166 ///
167 /// # Example
168 ///
169 /// ```rust,ignore
170 /// ctx.register_stream("log-stream", |chunk, sender| {
171 /// Box::pin(async move {
172 /// println!("Received chunk {} from {:?}", chunk.sequence, sender);
173 /// Ok(())
174 /// })
175 /// }).await?;
176 /// ```
177 async fn register_stream<F>(&self, stream_id: String, callback: F) -> ActorResult<()>
178 where
179 F: Fn(DataChunk, ActrId) -> MaybeSendBoxFuture<'static, ActorResult<()>>
180 + MaybeSendSync
181 + 'static;
182
183 /// Unregister a DataChunk callback
184 ///
185 /// # Parameters
186 ///
187 /// - `stream_id`: Stream identifier to unregister
188 async fn unregister_stream(&self, stream_id: &str) -> ActorResult<()>;
189
190 /// Send a DataChunk to a destination with explicit lane selection.
191 ///
192 /// Use [`PayloadType::StreamReliable`] for ordered reliable delivery (default) or
193 /// [`PayloadType::StreamLatencyFirst`] for low-latency partial-reliable delivery.
194 ///
195 /// # Parameters
196 ///
197 /// - `target`: Target destination
198 /// - `chunk`: DataChunk to send
199 /// - `payload_type`: Lane selection (`StreamReliable` or `StreamLatencyFirst`)
200 async fn send_data_chunk(
201 &self,
202 target: &crate::Dest,
203 chunk: DataChunk,
204 payload_type: PayloadType,
205 ) -> ActorResult<()>;
206
207 /// Discover a remote Actor of the specified type via the signaling server.
208 ///
209 /// Returns a route candidate or an error if none are available. Concrete
210 /// selection strategy is decided by the Context implementation.
211 async fn discover_route_candidate(&self, target_type: &ActrType) -> ActorResult<ActrId>;
212
213 /// Send a raw RPC request (untyped bytes) and wait for response
214 ///
215 /// This is a lower-level method for dynamic dispatch scenarios where the
216 /// request/response types are not known at compile time (e.g., FFI bindings).
217 ///
218 /// # Parameters
219 ///
220 /// - `target`: Target Actor ID
221 /// - `route_key`: Route key (e.g., "echo.EchoService/Echo")
222 /// - `payload`: Raw request payload bytes
223 ///
224 /// # Returns
225 ///
226 /// Returns raw response payload bytes
227 ///
228 /// # Example
229 ///
230 /// ```rust,ignore
231 /// // For FFI or dynamic dispatch scenarios
232 /// let response = ctx.call_raw(
233 /// &target_id,
234 /// "echo.EchoService/Echo",
235 /// request_bytes.into(),
236 /// ).await?;
237 /// ```
238 async fn call_raw(
239 &self,
240 target: &ActrId,
241 route_key: &str,
242 payload: bytes::Bytes,
243 ) -> ActorResult<bytes::Bytes>;
244
245 // ========== Fast Path: MediaTrack Methods (WebRTC Native) ==========
246
247 /// Register a WebRTC native media track callback
248 ///
249 /// When media samples arrive on the specified track, the registered callback will be invoked.
250 /// Uses WebRTC native RTCTrackRemote, no protobuf serialization overhead.
251 ///
252 /// # Parameters
253 ///
254 /// - `track_id`: Media track identifier (must match WebRTC track ID in SDP)
255 /// - `callback`: Handler function that receives native media samples
256 ///
257 /// # Example
258 ///
259 /// ```rust,ignore
260 /// use actr_framework::MediaSample;
261 ///
262 /// ctx.register_media_track("video-track-1", |sample, sender| {
263 /// Box::pin(async move {
264 /// // Decode and render video frame (native RTP payload)
265 /// println!("Received {} bytes at timestamp {}",
266 /// sample.data.len(), sample.timestamp);
267 /// decoder.decode(&sample.data).await?;
268 /// Ok(())
269 /// })
270 /// }).await?;
271 /// ```
272 ///
273 /// # Architecture Note
274 ///
275 /// MediaTrack uses WebRTC native RTP channels (RTCTrackRemote), NOT DataChannel.
276 /// This provides:
277 /// - Zero protobuf serialization overhead
278 /// - Native RTP header information (timestamp, SSRC, etc.)
279 /// - Optimal latency (~1-2ms lower than DataChannel)
280 async fn register_media_track<F>(&self, track_id: String, callback: F) -> ActorResult<()>
281 where
282 F: Fn(MediaSample, ActrId) -> BoxFuture<'static, ActorResult<()>> + Send + Sync + 'static;
283
284 /// Unregister a media track callback
285 ///
286 /// # Parameters
287 ///
288 /// - `track_id`: Media track identifier to unregister
289 async fn unregister_media_track(&self, track_id: &str) -> ActorResult<()>;
290
291 /// Send media samples via WebRTC native track
292 ///
293 /// Sends raw media samples through WebRTC RTCRtpSender (native RTP).
294 /// This is much more efficient than sending through DataChannel.
295 ///
296 /// # Parameters
297 ///
298 /// - `target`: Target destination
299 /// - `track_id`: Track identifier
300 /// - `sample`: Media sample to send
301 ///
302 /// # Example
303 ///
304 /// ```rust,ignore
305 /// use actr_framework::{MediaSample, MediaType};
306 ///
307 /// let sample = MediaSample {
308 /// data: encoded_frame.into(),
309 /// timestamp: rtp_timestamp,
310 /// codec: "H264".to_string(),
311 /// media_type: MediaType::Video,
312 /// };
313 ///
314 /// ctx.send_media_sample(&target, "video-track-1", sample).await?;
315 /// ```
316 async fn send_media_sample(
317 &self,
318 target: &crate::Dest,
319 track_id: &str,
320 sample: MediaSample,
321 ) -> ActorResult<()>;
322
323 /// Add a media track to the WebRTC connection with the target
324 ///
325 /// Creates a new RTP track on the PeerConnection and triggers SDP renegotiation.
326 /// Must be called before `send_media_sample()` for the given track.
327 ///
328 /// # Parameters
329 ///
330 /// - `target`: Target destination
331 /// - `track_id`: Media track identifier
332 /// - `codec`: Codec name (e.g., "VP8", "H264", "OPUS")
333 /// - `media_type`: Media type ("video" or "audio")
334 async fn add_media_track(
335 &self,
336 target: &crate::Dest,
337 track_id: &str,
338 codec: &str,
339 media_type: &str,
340 ) -> ActorResult<()>;
341
342 /// Remove a media track from the WebRTC connection with the target.
343 ///
344 /// If the track exists, this removes the RTP sender from the PeerConnection
345 /// and triggers SDP renegotiation so repeated start/stop cycles do not keep
346 /// stale tracks alive on the connection.
347 async fn remove_media_track(&self, target: &crate::Dest, track_id: &str) -> ActorResult<()>;
348
349 // ========== Observation ==========
350
351 /// Emit a log record from the workload side, routed through whichever
352 /// observability pipeline the runtime installs.
353 ///
354 /// The default implementation forwards to `tracing` using the configured
355 /// `target = "actr_framework::workload"`. Runtimes that embed workloads
356 /// in environments without `tracing` (e.g. `wasm32-unknown-unknown`
357 /// running in a browser Service Worker) override this to surface records
358 /// through whatever host hook is available (`console.log`, host import, ...).
359 fn log(&self, level: LogLevel, msg: &str) {
360 match level {
361 LogLevel::Trace => tracing::trace!(target: "actr_framework::workload", "{msg}"),
362 LogLevel::Debug => tracing::debug!(target: "actr_framework::workload", "{msg}"),
363 LogLevel::Info => tracing::info!(target: "actr_framework::workload", "{msg}"),
364 LogLevel::Warn => tracing::warn!(target: "actr_framework::workload", "{msg}"),
365 LogLevel::Error => tracing::error!(target: "actr_framework::workload", "{msg}"),
366 }
367 }
368}
369
370/// Severity level for [`Context::log`].
371///
372/// Mirrors the five standard levels exposed by `tracing` / `log` so runtimes
373/// can map to whatever sink is available on the target (native `tracing`
374/// subscriber, browser `console.*`, host-import log channel, ...).
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub enum LogLevel {
377 /// Very fine-grained diagnostic information, typically disabled in
378 /// production.
379 Trace,
380 /// Fine-grained information useful while debugging.
381 Debug,
382 /// Informational messages that mark normal operation.
383 Info,
384 /// Conditions that are unexpected but do not prevent continued
385 /// operation.
386 Warn,
387 /// Failures that likely require operator attention.
388 Error,
389}
390
391/// Media sample data from WebRTC native track
392///
393/// Lightweight wrapper around WebRTC native RTP sample.
394#[derive(Clone)]
395pub struct MediaSample {
396 /// Raw sample data (encoded audio/video frame)
397 pub data: bytes::Bytes,
398
399 /// Sample timestamp (from RTP timestamp)
400 pub timestamp: u32,
401
402 /// Codec-specific information
403 pub codec: String,
404
405 /// Media type (audio or video)
406 pub media_type: MediaType,
407}
408
409/// Media type enum
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum MediaType {
412 Audio,
413 Video,
414}