sycamore_query/
client.rs

1use fnv::{FnvBuildHasher, FnvHashMap};
2use std::{
3    rc::{Rc, Weak},
4    sync::RwLock,
5    time::Duration,
6};
7use sycamore::reactive::Signal;
8use weak_table::WeakValueHashMap;
9
10use crate::{cache::QueryCache, AsKeys, DataSignal, Fetcher, QueryData, Status};
11
12/// Global query options.
13/// These can be overridden on a per query basis with [`QueryOptions`].
14///
15/// # Options
16///
17/// * `cache_expiration` - The time before a cached query result expires.
18/// Default: 5 minutes
19/// * `retries` - The number of times to retry a query if it fails. Default: 3
20/// * `retry_fn` - The function for the timeout between retries. Defaults to
21/// exponential delay starting with 1 second, but not going over 30 seconds.
22///
23#[derive(Clone)]
24pub struct ClientOptions {
25    /// The time before a cached query result expires. Default: 5 minutes
26    pub cache_expiration: Duration,
27    /// The number of times to retry a query if it fails. Default: 3
28    pub retries: u32,
29    /// The function for the timeout between retries. Defaults to
30    /// exponential delay starting with 1 second, but not going over 30 seconds.
31    pub retry_fn: Rc<dyn Fn(u32) -> Duration>,
32}
33
34impl Default for ClientOptions {
35    fn default() -> Self {
36        Self {
37            cache_expiration: Duration::from_secs(5 * 60),
38            retries: 3,
39            retry_fn: Rc::new(|retries| {
40                Duration::from_secs((1 ^ (2 * retries)).clamp(0, 30) as u64)
41            }),
42        }
43    }
44}
45
46impl ClientOptions {
47    pub(crate) fn merge(&self, query_options: &QueryOptions) -> ClientOptions {
48        Self {
49            cache_expiration: query_options
50                .cache_expiration
51                .unwrap_or(self.cache_expiration),
52            retries: query_options.retries.unwrap_or(self.retries),
53            retry_fn: query_options
54                .retry_fn
55                .clone()
56                .unwrap_or_else(|| self.retry_fn.clone()),
57        }
58    }
59}
60
61/// Query-specific options that override the global [`ClientOptions`].
62/// Any fields that are not set are defaulted to the [`QueryClient`]'s settings.
63///
64/// # Options
65///
66/// * `cache_expiration` - The time before a cached query result expires.
67/// * `retries` - The number of times to retry a query if it fails. Default: 3
68/// * `retry_fn` - The function for the timeout between retries. Defaults to
69/// exponential delay starting with 1 second, but not going over 30 seconds.
70///
71#[derive(Default)]
72pub struct QueryOptions {
73    /// The time before a cached query result expires. Default: 5 minutes
74    pub cache_expiration: Option<Duration>,
75    /// The number of times to retry a query if it fails. Default: 3
76    pub retries: Option<u32>,
77    /// The function for the timeout between retries. Defaults to
78    /// exponential delay starting with 1 second, but not going over 30 seconds.
79    pub retry_fn: Option<Rc<dyn Fn(u32) -> Duration>>,
80}
81
82type WeakFnvMap<T> = WeakValueHashMap<Vec<u64>, Weak<T>, FnvBuildHasher>;
83
84/// The query client for `sycamore-query`. This stores your default settings,
85/// the cache and all queries that need to be updated when a query is refetched
86/// or updated. The client needs to be provided as a Context object in your top
87/// level component (`sycamore`) or index view (`perseus`).
88/// # Example
89///
90/// ```
91/// # use sycamore::prelude::*;
92/// # use sycamore_query::*;
93///
94/// #[component]
95/// pub fn App<G: Html>(cx: Scope) -> View<G> {
96///     let client = QueryClient::new(ClientOptions::default());
97///     provide_context(cx, client);
98///
99///     // You can now use the sycamore-query hooks
100///     view! { cx, }
101/// }
102/// ```
103///
104#[derive(Default)]
105pub struct QueryClient {
106    pub(crate) default_options: ClientOptions,
107    pub(crate) cache: RwLock<QueryCache>,
108    pub(crate) data_signals: RwLock<WeakFnvMap<DataSignal>>,
109    pub(crate) status_signals: RwLock<WeakFnvMap<Signal<Status>>>,
110    pub(crate) fetchers: RwLock<FnvHashMap<Vec<u64>, Fetcher>>,
111}
112
113impl QueryClient {
114    /// Creates a new QueryClient.
115    ///
116    /// # Arguments
117    /// * `default_options` - The global query options.
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// # use sycamore_query::*;
123    /// let client = QueryClient::new(ClientOptions::default());
124    /// ```
125    pub fn new(default_options: ClientOptions) -> Rc<Self> {
126        Rc::new(Self {
127            default_options,
128            ..QueryClient::default()
129        })
130    }
131
132    /// Invalidate all queries whose keys start with any of the keys passed in.
133    /// For example, passing a top level query ID will invalidate all queries
134    /// with that top level ID, regardless of their arguments.
135    /// For passing multiple keys with tuple types, see [`keys!`](crate::keys).
136    ///
137    /// # Example
138    ///
139    /// ```
140    /// # use sycamore_query::*;
141    /// # let client = QueryClient::new(ClientOptions::default());
142    /// // This will invalidate all queries whose keys start with `"hello"`,
143    /// // or where the first key is `"user"` and the first argument `3`
144    /// client.invalidate_queries(keys!["hello", ("user", 3)]);
145    /// ```
146    ///
147    pub fn invalidate_queries(self: Rc<Self>, queries: Vec<Vec<u64>>) {
148        let queries = queries
149            .iter()
150            .map(|query| query.as_slice())
151            .collect::<Vec<_>>();
152        self.cache.write().unwrap().invalidate_keys(&queries);
153        log::info!(
154            "Invalidating queries: {queries:?}. Queries in cache: {:?}",
155            self.data_signals.read().unwrap().keys().collect::<Vec<_>>()
156        );
157        for query in self
158            .data_signals
159            .read()
160            .unwrap()
161            .keys()
162            .filter(|k| queries.iter().any(|key| k.starts_with(key)))
163        {
164            log::info!("Updating query {query:?}");
165            if let Some((data, status, fetcher)) = self.find_query(query, false) {
166                log::info!("Query present. Running fetch.");
167                self.clone()
168                    .run_query(query, data, status, fetcher, &QueryOptions::default());
169            }
170        }
171    }
172
173    /// Collect garbage from the client cache
174    /// Call this whenever a lot of queries have been removed (i.e. on going to
175    /// a different page) to keep memory usage low.
176    /// Alternatively you could call this on a timer with the same length as your
177    /// cache expiration time.
178    ///
179    /// This will iterate through the entire cache sequentially, so don't use
180    /// on every frame.
181    pub fn collect_garbage(&self) {
182        self.cache.write().unwrap().collect_garbage();
183        // Queries get collected automatically, make sure to also collect fetchers
184        let queries = self.status_signals.read().unwrap();
185        self.fetchers
186            .write()
187            .unwrap()
188            .retain(|k, _| queries.contains_key(k));
189    }
190
191    /// Fetch query data from the cache if it exists. If it doesn't or the data
192    /// is expired, this will return `None`.
193    pub fn query_data<K: AsKeys, T: 'static>(&self, key: K) -> Option<Rc<T>> {
194        let data = self.cache.read().unwrap().get(&key.as_keys())?;
195        Some(data.clone().downcast().unwrap())
196    }
197
198    /// Override the query data in the cache for a given key. This will update
199    /// all queries with the same key automatically to reflect the new data.
200    pub fn set_query_data<K: AsKeys, T: 'static>(&self, key: K, value: T) {
201        let key = key.as_keys();
202        let value = Rc::new(value);
203        if let Some(data) = self.data_signals.read().unwrap().get(&key) {
204            data.set(QueryData::Ok(value.clone()))
205        }
206        self.cache
207            .write()
208            .unwrap()
209            .insert(key, Rc::new(value), &self.default_options);
210    }
211}