Skip to main content

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}