lastfm_edit/
events.rs

1//! Event system for monitoring HTTP client activity.
2//!
3//! This module provides comprehensive event broadcasting for observing internal
4//! HTTP client operations, including request lifecycle, rate limiting detection,
5//! and scrobble editing operations.
6
7use crate::edit::ExactScrobbleEdit;
8use serde::{Deserialize, Serialize};
9use tokio::sync::{broadcast, watch};
10
11/// Request information for client events
12#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct RequestInfo {
14    /// The HTTP method (GET, POST, etc.)
15    pub method: String,
16    /// The full URI being requested
17    pub uri: String,
18    /// Query parameters as key-value pairs
19    pub query_params: Vec<(String, String)>,
20    /// Path without query parameters
21    pub path: String,
22}
23
24impl RequestInfo {
25    /// Create RequestInfo from a URL string and method
26    pub fn from_url_and_method(url: &str, method: &str) -> Self {
27        // Parse URL manually to avoid adding dependencies
28        let (path, query_params) = if let Some(query_start) = url.find('?') {
29            let path = url[..query_start].to_string();
30            let query_string = &url[query_start + 1..];
31
32            let query_params: Vec<(String, String)> = query_string
33                .split('&')
34                .filter_map(|pair| {
35                    if let Some(eq_pos) = pair.find('=') {
36                        let key = &pair[..eq_pos];
37                        let value = &pair[eq_pos + 1..];
38                        Some((key.to_string(), value.to_string()))
39                    } else if !pair.is_empty() {
40                        Some((pair.to_string(), String::new()))
41                    } else {
42                        None
43                    }
44                })
45                .collect();
46
47            (path, query_params)
48        } else {
49            (url.to_string(), Vec::new())
50        };
51
52        // Extract just the path part if it's a full URL
53        let path = if path.starts_with("http://") || path.starts_with("https://") {
54            if let Some(third_slash) = path[8..].find('/') {
55                path[8 + third_slash..].to_string()
56            } else {
57                "/".to_string()
58            }
59        } else {
60            path
61        };
62
63        Self {
64            method: method.to_string(),
65            uri: url.to_string(),
66            query_params,
67            path,
68        }
69    }
70
71    /// Get a short description of the request for logging
72    pub fn short_description(&self) -> String {
73        let mut desc = format!("{} {}", self.method, self.path);
74        if !self.query_params.is_empty() {
75            let params: Vec<String> = self
76                .query_params
77                .iter()
78                .map(|(k, v)| format!("{k}={v}"))
79                .collect();
80            if params.len() <= 2 {
81                desc.push_str(&format!("?{}", params.join("&")));
82            } else {
83                desc.push_str(&format!("?{}...", params[0]));
84            }
85        }
86        desc
87    }
88}
89
90/// Type of rate limiting detected
91#[derive(Clone, Debug, Serialize, Deserialize)]
92pub enum RateLimitType {
93    /// HTTP 429 Too Many Requests
94    Http429,
95    /// HTTP 403 Forbidden (likely rate limiting)
96    Http403,
97    /// Rate limit patterns detected in response body
98    ResponsePattern,
99}
100
101/// Event type to describe internal HTTP client activity
102#[derive(Clone, Debug, Serialize, Deserialize)]
103pub enum ClientEvent {
104    /// Request started
105    RequestStarted {
106        /// Request details
107        request: RequestInfo,
108    },
109    /// Request completed successfully
110    RequestCompleted {
111        /// Request details
112        request: RequestInfo,
113        /// HTTP status code
114        status_code: u16,
115        /// Duration of the request in milliseconds
116        duration_ms: u64,
117    },
118    /// Rate limiting detected with backoff duration in seconds
119    RateLimited {
120        /// Duration to wait in seconds
121        delay_seconds: u64,
122        /// Request that triggered the rate limit (if available)
123        request: Option<RequestInfo>,
124        /// Type of rate limiting detected
125        rate_limit_type: RateLimitType,
126    },
127    /// Scrobble edit attempt completed
128    EditAttempted {
129        /// The exact scrobble edit that was attempted
130        edit: ExactScrobbleEdit,
131        /// Whether the edit was successful
132        success: bool,
133        /// Optional error message if the edit failed
134        error_message: Option<String>,
135        /// Duration of the edit operation in milliseconds
136        duration_ms: u64,
137    },
138}
139
140/// Type alias for the broadcast receiver
141pub type ClientEventReceiver = broadcast::Receiver<ClientEvent>;
142
143/// Type alias for the watch receiver
144pub type ClientEventWatcher = watch::Receiver<Option<ClientEvent>>;
145
146/// Shared event broadcasting state that persists across client clones
147#[derive(Clone)]
148pub struct SharedEventBroadcaster {
149    event_tx: broadcast::Sender<ClientEvent>,
150    last_event_tx: watch::Sender<Option<ClientEvent>>,
151}
152
153impl SharedEventBroadcaster {
154    /// Create a new shared event broadcaster
155    pub fn new() -> Self {
156        let (event_tx, _) = broadcast::channel(100);
157        let (last_event_tx, _) = watch::channel(None);
158
159        Self {
160            event_tx,
161            last_event_tx,
162        }
163    }
164
165    /// Broadcast an event to all subscribers
166    pub fn broadcast_event(&self, event: ClientEvent) {
167        let _ = self.event_tx.send(event.clone());
168        let _ = self.last_event_tx.send(Some(event));
169    }
170
171    /// Subscribe to events
172    pub fn subscribe(&self) -> ClientEventReceiver {
173        self.event_tx.subscribe()
174    }
175
176    /// Get the latest event
177    pub fn latest_event(&self) -> Option<ClientEvent> {
178        self.last_event_tx.borrow().clone()
179    }
180}
181
182impl Default for SharedEventBroadcaster {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188impl std::fmt::Debug for SharedEventBroadcaster {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        f.debug_struct("SharedEventBroadcaster")
191            .field("subscribers", &self.event_tx.receiver_count())
192            .finish()
193    }
194}