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}