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}