leptos_query_rs/query/
mod.rs

1//! Query Hooks and Options
2//!
3//! The main user-facing API for data fetching with reactive queries.
4
5use leptos::prelude::*;
6use leptos::task::spawn_local;
7use std::time::Duration;
8use std::future::Future;
9use serde::{Serialize, de::DeserializeOwned};
10
11use crate::client::QueryClient;
12use crate::retry::{QueryError, RetryConfig, execute_with_retry};
13use crate::types::{QueryStatus, QueryKey};
14
15/// Options for configuring a query
16#[derive(Clone)]
17pub struct QueryOptions {
18    /// Whether the query should run
19    pub enabled: bool,
20    /// Time before data becomes stale
21    pub stale_time: Duration,
22    /// Time before data is removed from cache
23    pub cache_time: Duration,
24    /// Interval for background refetching
25    pub refetch_interval: Option<Duration>,
26    /// Retry configuration
27    pub retry: RetryConfig,
28}
29
30impl Default for QueryOptions {
31    fn default() -> Self {
32        Self {
33            enabled: true,
34            stale_time: Duration::from_secs(0),
35            cache_time: Duration::from_secs(5 * 60), // 5 minutes
36            refetch_interval: None,
37            retry: RetryConfig::default(),
38        }
39    }
40}
41
42impl QueryOptions {
43    /// Create options with custom stale time
44    pub fn with_stale_time(mut self, duration: Duration) -> Self {
45        self.stale_time = duration;
46        self
47    }
48    
49    /// Create options with custom cache time
50    pub fn with_cache_time(mut self, duration: Duration) -> Self {
51        self.cache_time = duration;
52        self
53    }
54    
55    /// Create options with refetch interval
56    pub fn with_refetch_interval(mut self, interval: Duration) -> Self {
57        self.refetch_interval = Some(interval);
58        self
59    }
60    
61    /// Create options with retry configuration
62    pub fn with_retry(mut self, retry: RetryConfig) -> Self {
63        self.retry = retry;
64        self
65    }
66    
67    /// Disable the query by default
68    pub fn disabled(mut self) -> Self {
69        self.enabled = false;
70        self
71    }
72}
73
74/// Result of a query hook
75#[derive(Clone)]
76pub struct QueryResult<T: 'static + Send + Sync> {
77    /// The query data
78    pub data: Signal<Option<T>>,
79    /// Error if any
80    pub error: Signal<Option<QueryError>>,
81    /// Whether the query is loading
82    pub is_loading: Signal<bool>,
83    /// Whether the query succeeded
84    pub is_success: Signal<bool>,
85    /// Whether the query failed
86    pub is_error: Signal<bool>,
87    /// Current query status
88    pub status: Signal<QueryStatus>,
89    
90    // Actions
91    /// Refetch the query
92    pub refetch: Callback<()>,
93}
94
95/// Main query hook
96pub fn use_query<T, F, Fut>(
97    key_fn: F,
98    query_fn: impl Fn() -> Fut + Clone + Send + Sync + 'static,
99    options: QueryOptions,
100) -> QueryResult<T>
101where
102    T: Clone + Send + Sync + Serialize + DeserializeOwned + 'static,
103    F: Fn() -> QueryKey + Clone + Send + Sync + 'static,
104    Fut: Future<Output = Result<T, QueryError>> + 'static,
105{
106    // Create reactive state
107    let (data, set_data) = signal(None::<T>);
108    let (error, set_error) = signal(None::<QueryError>);
109    let (is_loading, set_loading) = signal(true);
110    let (status, set_status) = signal(QueryStatus::Loading);
111
112    // Get query client from context
113    let client = use_context::<QueryClient>().expect("QueryClient not found in context");
114    
115    // Create key signal
116    let key = Memo::new(move |_| key_fn());
117    
118    // Create fetch function for initial fetch
119    let initial_fetch = {
120        let client = client.clone();
121        let query_fn = query_fn.clone();
122        let options = options.clone();
123        
124        move |force: bool| {
125            let client = client.clone();
126            let query_fn = query_fn.clone();
127            let options = options.clone();
128            
129            spawn_local(async move {
130                let current_key = key.get();
131                
132                // Check cache first
133                if let Some(cache_entry) = client.get_cache_entry(&current_key) {
134                    if !force && !cache_entry.is_stale() {
135                        // Use cached data
136                        if let Ok(cached_data) = cache_entry.get_data::<T>() {
137                            set_data.set(Some(cached_data));
138                            set_loading.set(false);
139                            set_status.set(QueryStatus::Success);
140                            return;
141                        }
142                    }
143                }
144                
145                // Fetch new data
146                set_status.set(QueryStatus::Loading);
147                
148                let result = execute_with_retry(
149                    &query_fn,
150                    &options.retry,
151                ).await;
152                
153                match result {
154                    Ok(result_data) => {
155                        // Cache the data
156                        if let Ok(()) = client.set_query_data(&current_key, result_data.clone()) {
157                            set_data.set(Some(result_data));
158                            set_error.set(None);
159                            set_status.set(QueryStatus::Success);
160                        }
161                    }
162                    Err(err) => {
163                        set_error.set(Some(err.clone()));
164                        set_status.set(QueryStatus::Error);
165                    }
166                }
167                
168                set_loading.set(false);
169            });
170        }
171    };
172    
173    // Create fetch function for refetch
174    let refetch_fn = {
175        let client = client.clone();
176        let query_fn = query_fn.clone();
177        let options = options.clone();
178        
179        move |force: bool| {
180            let client = client.clone();
181            let query_fn = query_fn.clone();
182            let options = options.clone();
183            
184            spawn_local(async move {
185                let current_key = key.get();
186                
187                // Check cache first
188                if let Some(cache_entry) = client.get_cache_entry(&current_key) {
189                    if !force && !cache_entry.is_stale() {
190                        // Use cached data
191                        if let Ok(cached_data) = cache_entry.get_data::<T>() {
192                            set_data.set(Some(cached_data));
193                            set_loading.set(false);
194                            set_status.set(QueryStatus::Success);
195                            return;
196                        }
197                    }
198                }
199                
200                // Fetch new data
201                set_status.set(QueryStatus::Loading);
202                
203                let result = execute_with_retry(
204                    &query_fn,
205                    &options.retry,
206                ).await;
207                
208                match result {
209                    Ok(result_data) => {
210                        // Cache the data
211                        if let Ok(()) = client.set_query_data(&current_key, result_data.clone()) {
212                            set_data.set(Some(result_data));
213                            set_error.set(None);
214                            set_status.set(QueryStatus::Success);
215                        }
216                    }
217                    Err(err) => {
218                        set_error.set(Some(err.clone()));
219                        set_status.set(QueryStatus::Error);
220                    }
221                }
222                
223                set_loading.set(false);
224            });
225        }
226    };
227    
228    // Initial fetch
229    Effect::new(move |_| {
230        if options.enabled {
231            let current_key = key.get();
232            
233            // Check cache first
234            if let Some(cache_entry) = client.get_cache_entry(&current_key) {
235                if !cache_entry.is_stale() {
236                    // Use cached data
237                    if let Ok(cached_data) = cache_entry.get_data::<T>() {
238                        set_data.set(Some(cached_data));
239                        set_loading.set(false);
240                        set_status.set(QueryStatus::Success);
241                    }
242                } else {
243                    // Cache is stale, refetch
244                    initial_fetch(false);
245                }
246            } else {
247                // No cache, fetch immediately
248                initial_fetch(false);
249            }
250        }
251    });
252    
253    // Create computed signals
254    let is_success = Memo::new(move |_| status.get() == QueryStatus::Success);
255    let is_error = Memo::new(move |_| status.get() == QueryStatus::Error);
256    
257    // Create result
258    QueryResult {
259        data: data.into(),
260        error: error.into(),
261        is_loading: is_loading.into(),
262        is_success: is_success.into(),
263        is_error: is_error.into(),
264        status: status.into(),
265        refetch: Callback::new(move |_| refetch_fn(true)),
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    
273    #[test]
274    fn test_query_options_builder() {
275        let options = QueryOptions::default()
276            .with_stale_time(Duration::from_secs(60))
277            .with_cache_time(Duration::from_secs(300))
278            .disabled();
279        
280        assert_eq!(options.stale_time, Duration::from_secs(60));
281        assert_eq!(options.cache_time, Duration::from_secs(300));
282        assert!(!options.enabled);
283    }
284}