Skip to main content

gpui_query/hook/
options.rs

1//! Query and mutation options with builder pattern and sensible defaults.
2//!
3//! **v2**: All options use `Default` and `From<&str>` so users can pass just
4//! a string key for the simplest case.
5
6use std::sync::Arc;
7
8use crate::core::{CachePolicy, RefetchTrigger, RequestPolicy, RetryPolicy};
9
10/// Options for `use_query` and `fetch_query`.
11///
12/// # Quick Start
13///
14/// ```no_run
15/// use gpui_query::QueryOptions;
16/// use gpui_query::core::{CachePolicy, RetryPolicy};
17/// use gpui_query::hook::use_query;
18/// # #[derive(Clone)]
19/// # struct User;
20/// # #[derive(Clone, Debug)]
21/// # struct MyError;
22/// # fn _doc(cx: &mut gpui::Context<()>) {
23///
24/// // Simplest: just a string key
25/// let result = use_query("users", |signal| async move {
26///     Ok::<Vec<User>, MyError>(vec![])
27/// }, cx);
28///
29/// // With options:
30/// let result = use_query(
31///     QueryOptions::new("users")
32///         .cache_policy(CachePolicy::Ttl { ttl_ms: 300_000 })
33///         .retry_policy(RetryPolicy::new(5)),
34///     |signal| async move {
35///         Ok::<Vec<User>, MyError>(vec![])
36///     },
37///     cx,
38/// );
39/// # }
40/// ```
41#[derive(Clone, Debug)]
42pub struct QueryOptions {
43    /// The query key. Can be a string or multi-segment key.
44    pub key: crate::core::QueryKey,
45    /// Cache policy. Default: Ttl { ttl_ms: 60_000 }.
46    pub cache_policy: CachePolicy,
47    /// Request policy. Default: LatestWins.
48    pub request_policy: RequestPolicy,
49    /// Retry policy. Default: 3 retries with exponential backoff.
50    pub retry_policy: RetryPolicy,
51    /// GC time in milliseconds. Default: 300_000 (5 minutes).
52    ///
53    /// **Reserved / forward-compat** (audit fix #80): settable via the
54    /// `.gc_time(ms)` builder, but **not yet consumed** by `use_query`,
55    /// `fetch_query`, or the bucket layer. Garbage collection currently runs
56    /// off the global GC time set via [`QueryClient::with_gc_time`]; this
57    /// per-query value is stored only so a future release can honor it without
58    /// a breaking API change. Setting it has no effect today.
59    pub gc_time_ms: u64,
60    /// Whether to keep previous data when the key changes.
61    ///
62    /// **Reserved / forward-compat** (audit fix #80): settable via the
63    /// `.keep_previous()` builder, but **not yet consumed** by `use_query` or
64    /// `use_query_manual`. The `placeholderData`/`keepPreviousData` behavior is
65    /// not yet implemented; the field is stored so a future release can honor
66    /// it without a breaking API change. Setting it has no effect today.
67    pub keep_previous_data: bool,
68    /// Whether to force a fetch (ignore cache).
69    ///
70    /// When `true`, `use_query` passes `QueryFetchMode::Force` to
71    /// `begin_request`, bypassing cache freshness checks and always starting
72    /// a new fetch.
73    pub force_fetch: bool,
74    /// Refetch on mount trigger.
75    ///
76    /// **Reserved / forward-compat** (audit fix #80): settable on the struct,
77    /// but **not yet consumed**. The GPUI event-system integration for
78    /// automatic refetching on component mount is not yet implemented; the
79    /// field is stored so a future release can honor it without a breaking API
80    /// change. Setting it has no effect today.
81    pub refetch_on_mount: RefetchTrigger,
82    /// Refetch on window focus trigger.
83    ///
84    /// **Reserved / forward-compat** (audit fix #80): settable on the struct,
85    /// but **not yet consumed**. The GPUI event-system integration for
86    /// automatic refetching on window focus is not yet implemented; the field
87    /// is stored so a future release can honor it without a breaking API
88    /// change. Setting it has no effect today.
89    pub refetch_on_window_focus: RefetchTrigger,
90    /// Refetch on reconnect trigger.
91    ///
92    /// **Reserved / forward-compat** (audit fix #80): settable on the struct,
93    /// but **not yet consumed**. The GPUI event-system integration for
94    /// automatic refetching on reconnect is not yet implemented; the field is
95    /// stored so a future release can honor it without a breaking API change.
96    /// Setting it has no effect today.
97    pub refetch_on_reconnect: RefetchTrigger,
98}
99
100impl Default for QueryOptions {
101    fn default() -> Self {
102        Self {
103            // #77: The default key cannot be a `const` because `QueryKey`
104            // wraps an `Arc<[Arc<str>]>` and `Arc::from` is not const-stable,
105            // so `QueryKey` itself is not const-constructable. This is a
106            // single allocation per `QueryOptions::default()` call and is not
107            // on a hot path (`Default` is only invoked when a caller opts out
108            // of supplying a key, e.g. `use_mutation((), cx)`), so the runtime
109            // cost is acceptable. The `Arc` also means cloning the resulting
110            // default key is a single refcount bump.
111            key: crate::core::QueryKey::from("default"),
112            cache_policy: CachePolicy::default(),
113            request_policy: RequestPolicy::default(),
114            retry_policy: RetryPolicy::default(),
115            gc_time_ms: 300_000,
116            keep_previous_data: false,
117            force_fetch: false,
118            refetch_on_mount: RefetchTrigger::default(),
119            refetch_on_window_focus: RefetchTrigger::default(),
120            refetch_on_reconnect: RefetchTrigger::default(),
121        }
122    }
123}
124
125/// Declarative macro that generates the byte-for-byte equivalent builder
126/// methods shared by [`QueryOptions`] and [`InfiniteQueryOptions`]
127/// (`cache_policy`, `request_policy`, `retry_policy`, `gc_time`).
128///
129/// Audit fix #44: collapses the duplicated builders into a single source of
130/// truth so the two option types cannot drift.
131macro_rules! impl_query_options_builders {
132    ($t:ident) => {
133        impl $t {
134            /// Set the cache policy.
135            pub fn cache_policy(mut self, policy: CachePolicy) -> Self {
136                self.cache_policy = policy;
137                self
138            }
139
140            /// Set the request policy.
141            pub fn request_policy(mut self, policy: RequestPolicy) -> Self {
142                self.request_policy = policy;
143                self
144            }
145
146            /// Set the retry policy.
147            pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
148                self.retry_policy = policy;
149                self
150            }
151
152            /// Set the GC time in milliseconds.
153            pub fn gc_time(mut self, ms: u64) -> Self {
154                self.gc_time_ms = ms;
155                self
156            }
157        }
158    };
159}
160
161impl QueryOptions {
162    /// Create options with just a key.
163    pub fn new(key: impl Into<crate::core::QueryKey>) -> Self {
164        // Construct directly to avoid Default::default() allocating a default
165        // key that is immediately overwritten (audit H4).
166        Self {
167            key: key.into(),
168            cache_policy: CachePolicy::default(),
169            request_policy: RequestPolicy::default(),
170            retry_policy: RetryPolicy::default(),
171            gc_time_ms: 300_000,
172            keep_previous_data: false,
173            force_fetch: false,
174            refetch_on_mount: RefetchTrigger::default(),
175            refetch_on_window_focus: RefetchTrigger::default(),
176            refetch_on_reconnect: RefetchTrigger::default(),
177        }
178    }
179
180    /// Force a fetch, ignoring cache.
181    ///
182    /// When set, `use_query` passes `QueryFetchMode::Force` to `begin_request`,
183    /// which bypasses cache freshness checks and always starts a new fetch.
184    pub fn force(mut self) -> Self {
185        self.force_fetch = true;
186        self
187    }
188
189    /// Keep previous data when the key changes.
190    ///
191    /// **Reserved / forward-compat** (audit fix #80): sets the
192    /// `keep_previous_data` field, which is **not yet consumed** by `use_query`
193    /// or `use_query_manual`. The `keepPreviousData` behavior is intended for a
194    /// future release (preserve the prior `data`/`previous_data` slot across a
195    /// key change so the component keeps rendering the last successful result
196    /// while the new fetch is in flight). The builder is provided now so callers
197    /// can opt in without a future API change; calling it has no effect today.
198    pub fn keep_previous(mut self) -> Self {
199        self.keep_previous_data = true;
200        self
201    }
202}
203
204impl_query_options_builders!(QueryOptions);
205
206impl From<&str> for QueryOptions {
207    fn from(key: &str) -> Self {
208        Self::new(key)
209    }
210}
211
212impl From<String> for QueryOptions {
213    fn from(key: String) -> Self {
214        Self::new(key)
215    }
216}
217
218impl From<crate::core::QueryKey> for QueryOptions {
219    fn from(key: crate::core::QueryKey) -> Self {
220        Self::new(key)
221    }
222}
223
224/// Build [`QueryOptions`] from a raw `(key, cache_policy, request_policy)`
225/// triple.
226///
227/// Audit fix #79: this lets `use_query_manual_opts` /
228/// `use_query_unsignalled_opts` accept callers that already hold the legacy
229/// raw-parameter triple without forcing them to spell out `QueryOptions::new`.
230/// Non-breaking: the existing constructors and `From` impls are untouched.
231impl From<(crate::core::QueryKey, CachePolicy, RequestPolicy)> for QueryOptions {
232    fn from(
233        (key, cache_policy, request_policy): (crate::core::QueryKey, CachePolicy, RequestPolicy),
234    ) -> Self {
235        Self {
236            key,
237            cache_policy,
238            request_policy,
239            ..Default::default()
240        }
241    }
242}
243
244/// Options for `use_mutation`.
245#[derive(Clone, Debug)]
246pub struct MutationOptions {
247    /// Retry policy. Default: no retries.
248    pub retry_policy: RetryPolicy,
249    /// GC time in milliseconds.
250    pub gc_time_ms: u64,
251}
252
253impl Default for MutationOptions {
254    fn default() -> Self {
255        Self {
256            retry_policy: RetryPolicy::no_retries(),
257            gc_time_ms: 300_000,
258        }
259    }
260}
261
262impl MutationOptions {
263    /// Set the retry policy.
264    ///
265    /// Audit fix #43: Mirrors the `.retry_policy(p)` builder on
266    /// [`QueryOptions`] so mutation callers can configure retries without
267    /// constructing `MutationOptions` via struct literal.
268    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
269        self.retry_policy = policy;
270        self
271    }
272
273    /// Set the GC time in milliseconds.
274    ///
275    /// Audit fix #43: Mirrors the `.gc_time(ms)` builder on [`QueryOptions`].
276    pub fn gc_time(mut self, ms: u64) -> Self {
277        self.gc_time_ms = ms;
278        self
279    }
280}
281
282/// Type alias for the `on_success` callback field on [`MutationCallbacks`].
283///
284/// Audit fix #96: collapses the `Option<Arc<dyn Fn(&T) + Send + Sync>>`
285/// field type so `clippy::type_complexity` does not fire on the struct
286/// definition.
287pub type MutationSuccessCallback<T> = Option<Arc<dyn Fn(&T) + Send + Sync>>;
288
289/// Type alias for the `on_error` callback field on [`MutationCallbacks`].
290pub type MutationErrorCallback<E> = Option<Arc<dyn Fn(&E) + Send + Sync>>;
291
292/// Type alias for the `on_settled` callback field on [`MutationCallbacks`].
293pub type MutationSettledCallback<T, E> = Option<Arc<dyn Fn(Option<&T>, Option<&E>) + Send + Sync>>;
294
295/// Lifecycle callbacks for mutations.
296///
297/// `Clone` is implemented manually (no `T: Clone` / `E: Clone` bound needed)
298/// because every field is an `Option<Arc<...>>` — cloning bumps the refcount,
299/// it does not clone `T`/`E`. Construct with `MutationCallbacks::new()` and
300/// the builder methods.
301///
302/// Callbacks are wrapped in `Arc` so they can be shared across concurrent
303/// mutation invocations. `E` should implement `std::fmt::Debug` so that
304/// callbacks can log or display error details.
305pub struct MutationCallbacks<T, E> {
306    /// Fired on terminal success (after all retries skipped or succeeded).
307    pub on_success: MutationSuccessCallback<T>,
308    /// Fired on terminal failure (after retries exhausted or cancelled).
309    pub on_error: MutationErrorCallback<E>,
310    /// Fired on every terminal outcome (success, failure, or discard).
311    pub on_settled: MutationSettledCallback<T, E>,
312}
313
314impl<T, E> Clone for MutationCallbacks<T, E> {
315    fn clone(&self) -> Self {
316        Self {
317            on_success: self.on_success.clone(),
318            on_error: self.on_error.clone(),
319            on_settled: self.on_settled.clone(),
320        }
321    }
322}
323
324impl<T, E> Default for MutationCallbacks<T, E> {
325    fn default() -> Self {
326        Self {
327            on_success: None,
328            on_error: None,
329            on_settled: None,
330        }
331    }
332}
333
334impl<T, E> MutationCallbacks<T, E> {
335    /// Create empty callbacks.
336    pub fn new() -> Self {
337        Self::default()
338    }
339
340    /// Set the success callback.
341    pub fn on_success(mut self, f: impl Fn(&T) + Send + Sync + 'static) -> Self {
342        self.on_success = Some(Arc::new(f));
343        self
344    }
345
346    /// Set the error callback.
347    pub fn on_error(mut self, f: impl Fn(&E) + Send + Sync + 'static) -> Self {
348        self.on_error = Some(Arc::new(f));
349        self
350    }
351
352    /// Set the settled callback (fires on both success and failure).
353    pub fn on_settled(
354        mut self,
355        f: impl Fn(Option<&T>, Option<&E>) + Send + Sync + 'static,
356    ) -> Self {
357        self.on_settled = Some(Arc::new(f));
358        self
359    }
360}
361
362/// Options for infinite queries.
363#[derive(Clone, Debug)]
364pub struct InfiniteQueryOptions {
365    /// The query key.
366    pub key: crate::core::QueryKey,
367    /// Cache policy.
368    pub cache_policy: CachePolicy,
369    /// Request policy.
370    pub request_policy: RequestPolicy,
371    /// Maximum pages to retain. Default: 50.
372    pub max_pages: Option<usize>,
373    /// Retry policy.
374    pub retry_policy: RetryPolicy,
375    /// GC time in milliseconds. Default: 300_000 (5 minutes).
376    pub gc_time_ms: u64,
377}
378
379impl Default for InfiniteQueryOptions {
380    fn default() -> Self {
381        Self {
382            key: crate::core::QueryKey::from("default"),
383            cache_policy: CachePolicy::default(),
384            request_policy: RequestPolicy::default(),
385            max_pages: Some(50),
386            retry_policy: RetryPolicy::default(),
387            gc_time_ms: 300_000,
388        }
389    }
390}
391
392impl InfiniteQueryOptions {
393    /// Create with just a key.
394    pub fn new(key: impl Into<crate::core::QueryKey>) -> Self {
395        // Construct directly to avoid Default::default() allocating a default
396        // key that is immediately overwritten (audit H4).
397        Self {
398            key: key.into(),
399            cache_policy: CachePolicy::default(),
400            request_policy: RequestPolicy::default(),
401            max_pages: Some(50),
402            retry_policy: RetryPolicy::default(),
403            gc_time_ms: 300_000,
404        }
405    }
406
407    /// Set max pages. Pass a concrete number to cap retained pages.
408    ///
409    /// To allow unbounded pages, use [`InfiniteQueryOptions::unbounded_pages`]
410    /// instead.
411    pub fn max_pages(mut self, max: usize) -> Self {
412        self.max_pages = Some(max);
413        self
414    }
415
416    /// Allow unbounded page accumulation (no limit).
417    ///
418    /// Sets `max_pages` to `None`, meaning the infinite query will never
419    /// evict old pages. Use with caution — unbounded page storage can grow
420    /// without limit if the user scrolls far enough.
421    pub fn unbounded_pages(mut self) -> Self {
422        self.max_pages = None;
423        self
424    }
425}
426
427impl_query_options_builders!(InfiniteQueryOptions);
428
429impl From<&str> for InfiniteQueryOptions {
430    fn from(key: &str) -> Self {
431        Self::new(key)
432    }
433}
434
435impl From<String> for InfiniteQueryOptions {
436    fn from(key: String) -> Self {
437        Self::new(key)
438    }
439}
440
441impl From<crate::core::QueryKey> for InfiniteQueryOptions {
442    fn from(key: crate::core::QueryKey) -> Self {
443        Self::new(key)
444    }
445}