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}