Skip to main content

myko/core/
request.rs

1//! Request context for tracing operations across the Myko system.
2//!
3//! [`RequestContext`] carries identity and tracing information through command,
4//! query, and report execution. It enables:
5//!
6//! - **Transaction correlation**: All operations in a request share the same `tx`
7//! - **Call tracing**: `lineage` tracks the chain of operations (e.g., `["client", "CreateScene", "CreateBinding"]`)
8//! - **Client identification**: `client_id` identifies the WebSocket connection that initiated the request
9//! - **Server identification**: `host_id` identifies which server is processing the request
10//!
11//! # Example
12//!
13//! ```rust,no_run
14//! use std::sync::Arc;
15//! use uuid::Uuid;
16//! use myko::request::RequestContext;
17//!
18//! // Create initial context from WebSocket request
19//! let tx: Arc<str> = "tx-1".into();
20//! let client_id: Arc<str> = "client-1".into();
21//! let host_id = Uuid::new_v4();
22//! let ctx = RequestContext::from_client(tx, client_id, host_id);
23//!
24//! // Extend lineage for nested operations
25//! let child_ctx = ctx.child("CreateBinding");
26//! // child_ctx.lineage = ["client", "CreateBinding"]
27//! ```
28
29use std::sync::Arc;
30
31use uuid::Uuid;
32
33use crate::entities::client::ClientId;
34
35/// Context that propagates through request processing.
36///
37/// Created when a request arrives via WebSocket and flows through
38/// command execution, query subscriptions, and report generation.
39#[derive(Clone, Debug)]
40pub struct RequestContext {
41    /// Transaction ID - same across all operations in one request.
42    /// Used to correlate logs, events, and responses.
43    pub tx: Arc<str>,
44
45    /// Client ID of the WebSocket connection that initiated this request.
46    /// `None` for internal operations (sagas, startup tasks).
47    pub client_id: Option<ClientId>,
48
49    /// Call chain tracking the path of execution.
50    /// Starts with `["client"]` for WebSocket requests or `["saga", "SagaName"]` for sagas.
51    /// Extended via `child()` for nested operations.
52    pub lineage: Vec<Arc<str>>,
53
54    /// Server that received the original request.
55    pub host_id: Uuid,
56
57    /// ISO timestamp when the request started.
58    pub created_at: String,
59
60    /// Windback timestamp for historical state viewing.
61    /// When set, queries should return state as of this ISO timestamp.
62    /// `None` means the client is viewing live state.
63    pub windback: Option<Arc<str>>,
64}
65
66impl RequestContext {
67    /// Create a new RequestContext with explicit values.
68    pub fn new(
69        tx: Arc<str>,
70        client_id: Option<Arc<str>>,
71        lineage: Vec<Arc<str>>,
72        host_id: Uuid,
73        created_at: String,
74    ) -> Self {
75        Self {
76            tx,
77            client_id: client_id.map(Into::into),
78            lineage,
79            host_id,
80            created_at,
81            windback: None,
82        }
83    }
84
85    /// Create a context for a client-initiated request.
86    ///
87    /// Sets lineage to `["client"]` and created_at to current time.
88    /// Use `from_client_with_windback` to include windback state.
89    pub fn from_client(tx: Arc<str>, client_id: Arc<str>, host_id: Uuid) -> Self {
90        Self {
91            tx,
92            client_id: Some(client_id.into()),
93            lineage: vec![Arc::from("client")],
94            host_id,
95            created_at: chrono::Utc::now().to_rfc3339(),
96            windback: None,
97        }
98    }
99
100    /// Create a context for a client-initiated request with windback state.
101    ///
102    /// Sets lineage to `["client"]` and created_at to current time.
103    pub fn from_client_with_windback(
104        tx: Arc<str>,
105        client_id: Arc<str>,
106        host_id: Uuid,
107        windback: Option<Arc<str>>,
108    ) -> Self {
109        Self {
110            tx,
111            client_id: Some(client_id.into()),
112            lineage: vec![Arc::from("client")],
113            host_id,
114            created_at: chrono::Utc::now().to_rfc3339(),
115            windback,
116        }
117    }
118
119    /// Create a context for an internal operation (no client).
120    ///
121    /// Used for saga-initiated operations, startup tasks, etc.
122    pub fn internal(tx: Arc<str>, host_id: Uuid, origin: &str) -> Self {
123        Self {
124            tx,
125            client_id: None,
126            lineage: vec![Arc::from(origin)],
127            host_id,
128            created_at: chrono::Utc::now().to_rfc3339(),
129            windback: None,
130        }
131    }
132
133    /// Create a child context with extended lineage.
134    ///
135    /// Used when making sub-operations (nested commands, queries from reports, etc.)
136    /// to track the call chain. Preserves the parent's windback state.
137    ///
138    /// # Example
139    ///
140    /// ```rust,no_run
141    /// use std::sync::Arc;
142    /// use uuid::Uuid;
143    /// use myko::request::RequestContext;
144    ///
145    /// let tx: Arc<str> = "tx-1".into();
146    /// let client_id: Arc<str> = "client-1".into();
147    /// let host_id = Uuid::new_v4();
148    /// let parent = RequestContext::from_client(tx, client_id, host_id);
149    /// let child = parent.child("CreateBinding");
150    /// assert_eq!(child.lineage.len(), 2);
151    /// assert_eq!(child.lineage[0].as_ref(), "client");
152    /// assert_eq!(child.lineage[1].as_ref(), "CreateBinding");
153    /// ```
154    pub fn child(&self, operation: &str) -> Self {
155        let mut lineage = self.lineage.clone();
156        lineage.push(Arc::from(operation));
157        Self {
158            tx: self.tx.clone(),
159            client_id: self.client_id.clone(),
160            lineage,
161            host_id: self.host_id,
162            created_at: self.created_at.clone(),
163            windback: self.windback.clone(),
164        }
165    }
166
167    /// Check if this context is in windback mode.
168    pub fn is_windback(&self) -> bool {
169        self.windback.is_some()
170    }
171
172    /// Get the windback timestamp if set.
173    pub fn windback(&self) -> Option<&str> {
174        self.windback.as_deref()
175    }
176
177    /// Get the transaction ID as a string slice.
178    pub fn tx(&self) -> &str {
179        &self.tx
180    }
181
182    /// Get the client ID if present.
183    pub fn client_id(&self) -> Option<&str> {
184        self.client_id.as_deref()
185    }
186
187    /// Dispatch origin — the first lineage element (`"client"` for native-WS
188    /// requests and MCP-stdio-over-WS, `"mcp"` for MCP HTTP/WS in-process,
189    /// `"saga"` for saga-initiated operations). Used to tag dispatch metrics
190    /// by transport/origin.
191    pub fn origin(&self) -> &str {
192        self.lineage
193            .first()
194            .map(|s| s.as_ref())
195            .unwrap_or("unknown")
196    }
197
198    /// Get the lineage as a string for logging.
199    pub fn lineage_string(&self) -> String {
200        self.lineage
201            .iter()
202            .map(|s| s.as_ref())
203            .collect::<Vec<_>>()
204            .join(" → ")
205    }
206}