1use std::cell::RefCell;
25use std::sync::{Arc, Mutex};
26
27use serde_json::{json, Value as JsonValue};
28
29pub type OutboundFn = Arc<dyn Fn(JsonValue) + Send + Sync>;
36
37#[derive(Clone)]
42pub struct ProgressBus {
43 outbound: OutboundFn,
44 last_progress: Arc<Mutex<std::collections::HashMap<String, f64>>>,
45}
46
47impl std::fmt::Debug for ProgressBus {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("ProgressBus").finish_non_exhaustive()
50 }
51}
52
53impl ProgressBus {
54 pub fn new(outbound: OutboundFn) -> Self {
55 Self {
56 outbound,
57 last_progress: Arc::new(Mutex::new(std::collections::HashMap::new())),
58 }
59 }
60
61 pub fn from_mpsc(tx: tokio::sync::mpsc::UnboundedSender<JsonValue>) -> Self {
64 Self::new(Arc::new(move |message| {
65 let _ = tx.send(message);
66 }))
67 }
68
69 pub fn report(
77 &self,
78 token: &JsonValue,
79 progress: f64,
80 total: Option<f64>,
81 message: Option<String>,
82 ) -> bool {
83 if !is_valid_progress_token(token) {
84 return false;
85 }
86 if !progress.is_finite() {
87 return false;
88 }
89 if let Some(total) = total {
90 if !total.is_finite() {
91 return false;
92 }
93 }
94 let key = canonical_token(token);
95 {
96 let mut last = self.last_progress.lock().expect("progress map poisoned");
97 if let Some(previous) = last.get(&key).copied() {
98 if progress <= previous {
99 return false;
100 }
101 }
102 last.insert(key, progress);
103 }
104 let mut params = serde_json::Map::new();
105 params.insert("progressToken".to_string(), token.clone());
106 params.insert("progress".to_string(), json!(progress));
107 if let Some(total) = total {
108 params.insert("total".to_string(), json!(total));
109 }
110 if let Some(message) = message {
111 params.insert("message".to_string(), JsonValue::String(message));
112 }
113 (self.outbound)(crate::jsonrpc::notification(
114 "notifications/progress",
115 JsonValue::Object(params),
116 ));
117 true
118 }
119}
120
121#[derive(Clone, Debug)]
126pub struct ProgressContext {
127 pub bus: ProgressBus,
128 pub token: JsonValue,
129}
130
131impl ProgressContext {
132 pub fn new(bus: ProgressBus, token: JsonValue) -> Self {
133 Self { bus, token }
134 }
135
136 pub fn report(&self, progress: f64, total: Option<f64>, message: Option<String>) -> bool {
137 self.bus.report(&self.token, progress, total, message)
138 }
139}
140
141tokio::task_local! {
142 static CURRENT_CONTEXT: ProgressContext;
151}
152
153thread_local! {
154 static ACTIVE_BUS: RefCell<Option<ProgressBus>> = const { RefCell::new(None) };
155}
156
157pub async fn scope_context<F>(ctx: Option<ProgressContext>, future: F) -> F::Output
163where
164 F: std::future::Future,
165{
166 match ctx {
167 Some(ctx) => CURRENT_CONTEXT.scope(ctx, future).await,
168 None => future.await,
169 }
170}
171
172pub fn current_context() -> Option<ProgressContext> {
174 CURRENT_CONTEXT.try_with(|ctx| ctx.clone()).ok()
175}
176
177pub fn install_active_bus(bus: Option<ProgressBus>) -> Option<ProgressBus> {
182 ACTIVE_BUS.with(|cell| std::mem::replace(&mut *cell.borrow_mut(), bus))
183}
184
185pub fn active_bus() -> Option<ProgressBus> {
187 ACTIVE_BUS.with(|cell| cell.borrow().clone())
188}
189
190pub struct ActiveBusGuard {
193 previous: Option<ProgressBus>,
194}
195
196impl ActiveBusGuard {
197 pub fn install(bus: Option<ProgressBus>) -> Self {
198 Self {
199 previous: install_active_bus(bus),
200 }
201 }
202}
203
204impl Drop for ActiveBusGuard {
205 fn drop(&mut self) {
206 install_active_bus(self.previous.take());
207 }
208}
209
210pub fn is_valid_progress_token(value: &JsonValue) -> bool {
214 matches!(value, JsonValue::String(_) | JsonValue::Number(_))
215}
216
217fn canonical_token(value: &JsonValue) -> String {
220 if let Some(s) = value.as_str() {
221 return s.to_string();
222 }
223 if let Some(n) = value.as_i64() {
224 return n.to_string();
225 }
226 if let Some(n) = value.as_u64() {
227 return n.to_string();
228 }
229 if let Some(n) = value.as_f64() {
230 return n.to_string();
231 }
232 value.to_string()
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use std::sync::Mutex;
239
240 fn capturing_bus() -> (ProgressBus, Arc<Mutex<Vec<JsonValue>>>) {
241 let captured: Arc<Mutex<Vec<JsonValue>>> = Arc::new(Mutex::new(Vec::new()));
242 let captured_for_sink = captured.clone();
243 let bus = ProgressBus::new(Arc::new(move |message| {
244 captured_for_sink
245 .lock()
246 .expect("captured progress poisoned")
247 .push(message);
248 }));
249 (bus, captured)
250 }
251
252 #[test]
253 fn reports_progress_with_monotonic_check() {
254 let (bus, captured) = capturing_bus();
255 assert!(bus.report(&json!("tok"), 0.25, Some(1.0), Some("a".into())));
256 assert!(bus.report(&json!("tok"), 0.5, Some(1.0), None));
257 assert!(!bus.report(&json!("tok"), 0.5, Some(1.0), None));
258 assert!(!bus.report(&json!("tok"), 0.4, Some(1.0), None));
259 let captured = captured.lock().unwrap();
260 assert_eq!(captured.len(), 2);
261 assert_eq!(captured[0]["method"], json!("notifications/progress"));
262 assert_eq!(captured[0]["params"]["progressToken"], json!("tok"));
263 assert_eq!(captured[0]["params"]["progress"], json!(0.25));
264 assert_eq!(captured[0]["params"]["total"], json!(1.0));
265 assert_eq!(captured[0]["params"]["message"], json!("a"));
266 assert!(captured[1]["params"].get("message").is_none());
267 }
268
269 #[test]
270 fn reports_progress_for_numeric_token_independently() {
271 let (bus, captured) = capturing_bus();
272 assert!(bus.report(&json!(1), 0.1, None, None));
273 assert!(bus.report(&json!("tok"), 0.05, None, None));
274 let captured = captured.lock().unwrap();
275 assert_eq!(captured.len(), 2);
276 }
277
278 #[test]
279 fn rejects_non_finite_or_invalid_token() {
280 let (bus, captured) = capturing_bus();
281 assert!(!bus.report(&JsonValue::Null, 0.1, None, None));
282 assert!(!bus.report(&json!(true), 0.1, None, None));
283 assert!(!bus.report(&json!("tok"), f64::NAN, None, None));
284 assert!(!bus.report(&json!("tok"), 0.1, Some(f64::INFINITY), None));
285 assert!(captured.lock().unwrap().is_empty());
286 }
287
288 #[tokio::test]
289 async fn scope_context_is_visible_inside_and_absent_outside() {
290 assert!(current_context().is_none());
291 let (bus, _) = capturing_bus();
292 let ctx = ProgressContext::new(bus, json!("tok"));
293 scope_context(Some(ctx), async {
294 assert!(current_context().is_some());
295 })
296 .await;
297 assert!(current_context().is_none());
298 }
299
300 #[tokio::test]
301 async fn scope_context_isolates_concurrent_tasks() {
302 let (bus, captured) = capturing_bus();
303 let ctx_a = ProgressContext::new(bus.clone(), json!("a"));
304 let ctx_b = ProgressContext::new(bus, json!("b"));
305 let task_a = scope_context(Some(ctx_a), async {
306 tokio::task::yield_now().await;
307 current_context().unwrap().token
308 });
309 let task_b = scope_context(Some(ctx_b), async {
310 tokio::task::yield_now().await;
311 current_context().unwrap().token
312 });
313 let (a, b) = tokio::join!(task_a, task_b);
314 assert_eq!(a, json!("a"));
315 assert_eq!(b, json!("b"));
316 assert!(captured.lock().unwrap().is_empty());
317 }
318}