1#![allow(deprecated)] use std::sync::{Mutex, PoisonError};
20use std::time::Duration;
21
22use serde::Serialize;
23use serde_json::{Map, Value, json};
24use tokio::sync::mpsc;
25
26use crate::context::Ctx;
27use crate::spec::{self, LoggingLevel, methods};
28
29const FLUSH_TIMEOUT: Duration = Duration::from_secs(1);
31
32#[derive(Debug, Clone)]
35pub(crate) struct NotifyChannel(pub(crate) mpsc::Sender<Vec<u8>>);
36
37pub(crate) fn severity(level: LoggingLevel) -> u8 {
39 match level {
40 LoggingLevel::Debug => 0,
41 LoggingLevel::Info => 1,
42 LoggingLevel::Notice => 2,
43 LoggingLevel::Warning => 3,
44 LoggingLevel::Error => 4,
45 LoggingLevel::Critical => 5,
46 LoggingLevel::Alert => 6,
47 LoggingLevel::Emergency => 7,
48 }
49}
50
51#[derive(Debug)]
53pub(crate) struct Notifier {
54 out: mpsc::Sender<Vec<u8>>,
55 progress_token: Option<spec::ProgressToken>,
56 log_level: Option<LoggingLevel>,
58 state: Mutex<State>,
59}
60
61#[derive(Debug, Default)]
62struct State {
63 last_progress: Option<f64>,
64 pending_progress: Option<Vec<u8>>,
66 closed: bool,
67}
68
69impl Notifier {
70 pub(crate) fn new(
71 channel: &NotifyChannel,
72 progress_token: Option<spec::ProgressToken>,
73 log_level: Option<LoggingLevel>,
74 ) -> Option<Self> {
75 (progress_token.is_some() || log_level.is_some()).then(|| Self {
76 out: channel.0.clone(),
77 progress_token,
78 log_level,
79 state: Mutex::default(),
80 })
81 }
82
83 fn state(&self) -> std::sync::MutexGuard<'_, State> {
84 self.state.lock().unwrap_or_else(PoisonError::into_inner)
85 }
86
87 fn progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
88 let Some(token) = &self.progress_token else {
89 return;
90 };
91 let mut state = self.state();
92 if state.closed
93 || !progress.is_finite()
94 || state.last_progress.is_some_and(|last| progress <= last)
95 {
96 return;
97 }
98 state.last_progress = Some(progress);
99 let mut params = Map::new();
100 params.insert("progressToken".to_owned(), json!(token));
101 params.insert("progress".to_owned(), json!(progress));
102 if let Some(total) = total.filter(|t| t.is_finite()) {
103 params.insert("total".to_owned(), json!(total));
104 }
105 if let Some(message) = message {
106 params.insert("message".to_owned(), json!(message));
107 }
108 let Some(message) = notification(methods::NOTIFICATIONS_PROGRESS, params) else {
109 return;
110 };
111 state.pending_progress = None;
113 if let Err(mpsc::error::TrySendError::Full(message)) = self.out.try_send(message) {
114 state.pending_progress = Some(message);
115 }
116 }
117
118 fn log(&self, level: LoggingLevel, logger: Option<&str>, data: Value) {
119 let Some(minimum) = self.log_level else {
120 return;
121 };
122 if severity(level) < severity(minimum) || self.state().closed {
123 return;
124 }
125 let mut params = Map::new();
126 params.insert("level".to_owned(), json!(level));
127 params.insert("data".to_owned(), data);
128 if let Some(logger) = logger {
129 params.insert("logger".to_owned(), json!(logger));
130 }
131 if let Some(message) = notification(methods::NOTIFICATIONS_MESSAGE, params) {
132 let _ = self.out.try_send(message);
134 }
135 }
136
137 pub(crate) async fn finish(&self) {
140 let pending = {
141 let mut state = self.state();
142 state.closed = true;
143 state.pending_progress.take()
144 };
145 if let Some(message) = pending {
146 let _ = tokio::time::timeout(FLUSH_TIMEOUT, self.out.send(message)).await;
147 }
148 }
149}
150
151fn notification(method: &str, params: Map<String, Value>) -> Option<Vec<u8>> {
152 serde_json::to_vec(&json!({ "jsonrpc": "2.0", "method": method, "params": params })).ok()
153}
154
155impl Ctx {
156 pub fn progress(&self, progress: f64, total: impl Into<Option<f64>>) {
171 if let Some(notifier) = self.notifier() {
172 notifier.progress(progress, total.into(), None);
173 }
174 }
175
176 pub fn progress_message(
178 &self,
179 progress: f64,
180 total: impl Into<Option<f64>>,
181 message: impl AsRef<str>,
182 ) {
183 if let Some(notifier) = self.notifier() {
184 notifier.progress(progress, total.into(), Some(message.as_ref()));
185 }
186 }
187
188 #[must_use]
190 pub fn wants_progress(&self) -> bool {
191 self.notifier().is_some_and(|n| n.progress_token.is_some())
192 }
193
194 #[deprecated(
201 note = "Logging is deprecated as of protocol version 2026-07-28 (SEP-2577); log to stderr with `tracing` instead."
202 )]
203 pub fn log(&self, level: LoggingLevel, data: impl Serialize) {
204 if let Some(notifier) = self.notifier()
205 && let Ok(data) = serde_json::to_value(data)
206 {
207 notifier.log(level, None, data);
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn channel(capacity: usize) -> (NotifyChannel, mpsc::Receiver<Vec<u8>>) {
217 let (tx, rx) = mpsc::channel(capacity);
218 (NotifyChannel(tx), rx)
219 }
220
221 fn decode(bytes: &[u8]) -> Value {
222 serde_json::from_slice(bytes).unwrap()
223 }
224
225 #[test]
226 fn nothing_is_built_without_a_request_for_notifications() {
227 let (tx, _rx) = channel(4);
228 assert!(Notifier::new(&tx, None, None).is_none());
229 }
230
231 #[tokio::test]
232 async fn progress_increases_and_keeps_the_latest_value() {
233 let (tx, mut rx) = channel(1);
234 let n = Notifier::new(&tx, Some(spec::ProgressToken::Integer(7)), None).unwrap();
235 n.progress(10.0, Some(100.0), None);
236 n.progress(5.0, Some(100.0), None); n.progress(50.0, Some(100.0), Some("half")); n.progress(90.0, Some(100.0), None); let first = decode(&rx.recv().await.unwrap());
240 assert_eq!(first["method"], "notifications/progress");
241 assert_eq!(
242 first["params"],
243 json!({ "progressToken": 7, "progress": 10.0, "total": 100.0 })
244 );
245 n.finish().await;
246 let last = decode(&rx.recv().await.unwrap());
247 assert_eq!(last["params"]["progress"], 90.0);
248 n.progress(100.0, Some(100.0), None);
250 assert!(rx.try_recv().is_err());
251 }
252
253 #[test]
254 fn logs_respect_the_level() {
255 let (tx, mut rx) = channel(8);
256 let n = Notifier::new(&tx, None, Some(LoggingLevel::Warning)).unwrap();
257 n.log(LoggingLevel::Info, None, json!("hidden"));
258 n.log(LoggingLevel::Error, Some("db"), json!({ "e": 1 }));
259 let message = decode(&rx.try_recv().unwrap());
260 assert_eq!(
261 message["params"],
262 json!({ "level": "error", "logger": "db", "data": { "e": 1 } })
263 );
264 assert!(rx.try_recv().is_err());
265 n.progress(1.0, None, None);
267 assert!(rx.try_recv().is_err());
268 }
269}