Skip to main content

gpui_query/hook/
mod.rs

1//! The `use_query` and `use_mutation` hooks — ergonomic query and mutation
2//! subscriptions for GPUI components.
3//!
4//! # v2 Improvements
5//!
6//! - Uses `QueryObserver` which returns `Option<Subscription>` instead of panicking
7//! - Signals are properly cancelled on `LatestWins` replacement and `reset()`
8//! - `AHashMap` in `QueryClient` for faster lookups
9//! - `MutationDiagnostic` is a real type in devtools
10//! - `max_pages` defaults to `Some(50)`
11//! - `QueryError` has full `Display` + `Error` impls
12//!
13//! # Query Usage (options-first)
14//!
15//! The primary API is **options-first** with sensible defaults. The fetcher
16//! always receives a [`QuerySignal`] for cooperative cancellation:
17//!
18//! ```no_run
19//! use gpui_query::hook::use_query;
20//! use gpui_query::{QueryOptions, CachePolicy, RequestPolicy};
21//! # #[derive(Clone)]
22//! # struct User;
23//! # #[derive(Clone, Debug)]
24//! # struct MyError;
25//!
26//! struct MyView {
27//!     users: gpui::Entity<gpui_query::QueryResource<Vec<User>, MyError>>,
28//!     _subscription: gpui::Subscription,
29//! }
30//!
31//! impl MyView {
32//!     fn new(cx: &mut gpui::Context<Self>) -> Self {
33//!         let (users, _subscription) = use_query(
34//!             QueryOptions::new("users")
35//!                 .cache_policy(CachePolicy::Ttl { ttl_ms: 60_000 })
36//!                 .request_policy(RequestPolicy::LatestWins),
37//!             |signal| async move {
38//!                 // Your async fetcher here
39//!                 Ok(vec![])
40//!             },
41//!             cx,
42//!         );
43//!         Self { users, _subscription }
44//!     }
45//! }
46//! ```
47//!
48//! For backward compatibility, [`use_query_unsignalled`] is available with a
49//! `Fn() -> Fut` fetcher that receives no signal. However, the signal-accepting
50//! `use_query` is the recommended default per the v2 "Signal-always" design goal.
51//!
52//! # Mutation Usage
53//!
54//! ```no_run
55//! use gpui_query::hook::{use_mutation, mutate};
56//! # #[derive(Clone)]
57//! # struct NewUser { name: String }
58//! # #[derive(Clone)]
59//! # struct User;
60//! # #[derive(Clone, Debug)]
61//! # struct MyError;
62//!
63//! struct MyView {
64//!     create_user: gpui::Entity<gpui_query::MutationResource<NewUser, User, MyError>>,
65//!     _subscription: gpui::Subscription,
66//! }
67//!
68//! impl MyView {
69//!     fn new(cx: &mut gpui::Context<Self>) -> Self {
70//!         let (entity, sub) = use_mutation((), cx);
71//!         Self { create_user: entity, _subscription: sub }
72//!     }
73//!
74//!     fn handle_submit(&mut self, name: String, cx: &mut gpui::Context<Self>) {
75//!         mutate(&self.create_user, NewUser { name }, |vars| async move {
76//!             Ok(User)
77//!         }, cx);
78//!     }
79//! }
80//! ```
81//!
82//! # WeakEntity Discard Behavior
83//!
84//! Throughout this module, [`gpui::WeakEntity::upgrade()`] is used inside async
85//! tasks to access the owning entity. If the owning component is unmounted while
86//! a fetch is in-flight, `upgrade()` returns `None` and the fetch result is
87//! **silently discarded**. This is intentional for cache-layer correctness (avoids
88//! writing to a dead entity), but callers who rely on side effects from fetch
89//! completion should be aware that no callback or notification fires in this case.
90//!
91//! # Signal Cancellation (Audit Finding #8)
92//!
93//! The `accept_current_request` guard is the authoritative protection against stale
94//! writes. A previous `signal.is_cancelled()` check after the fetcher returned was
95//! removed -- it was a best-effort optimization with a TOCTOU window that provided
96//! no guarantees. The two-phase protocol (accept + complete) correctly handles all
97//! cases where a newer request supersedes the current one.
98
99mod fetch_retry;
100mod gpui_compat;
101mod mutation_hooks;
102mod options;
103mod query_hooks;
104mod use_infinite_query;
105mod use_query_select;
106
107// Source-compat shim: read entities regardless of whether `read_with` returns
108// `R` (older gpui / git) or `Result<R>` (gpui 0.2.2 / crates.io).
109pub(crate) use gpui_compat::read_entity;
110
111// ── Re-exports from options ─────────────────────────────────────────────
112
113pub use options::{InfiniteQueryOptions, MutationCallbacks, MutationOptions, QueryOptions};
114
115// ── Re-exports from query_hooks ─────────────────────────────────────────
116
117pub use query_hooks::{
118    fetch_query, fetch_query_with_policy, fetch_query_with_signal, use_query, use_query_manual,
119    use_query_manual_opts, use_query_unsignalled, use_query_unsignalled_opts,
120    use_query_with_policy,
121};
122
123// ── Re-exports from use_infinite_query ───────────────────────────────────
124
125pub use use_infinite_query::{
126    fetch_next_page_infinite, fetch_previous_page_infinite, use_infinite_query,
127};
128
129// ── Re-exports from use_query_select ─────────────────────────────────────
130
131pub use use_query_select::use_query_select;
132
133// ── Re-exports from mutation_hooks ───────────────────────────────────────
134//
135// Audit fix #22: `use_mutation_with_options` is intentionally NOT re-exported
136// here. The deprecated function itself remains defined (and delegates to
137// `use_mutation`), but removing it from the `pub use` list stops the
138// `deprecated` lint from firing on the re-export. Existing callers that
139// import it via the full path still see the deprecation warning at the call
140// site.
141
142pub use mutation_hooks::{
143    mutate, mutate_arc, mutate_by_ref, mutate_with_callbacks, use_mutation, use_mutation_state,
144};
145
146// ── Utility ─────────────────────────────────────────────────────────────
147
148/// Returns current time as milliseconds since UNIX epoch.
149///
150/// Audit fix #20: This is the canonical implementation used across the hook
151/// layer. The private duplicate in `mutation_bucket.rs` (`now_ms`) should
152/// ideally be consolidated here or into a shared utility module.
153///
154/// # Clock-before-epoch fallback
155///
156/// `duration_since(UNIX_EPOCH)` errors if the system clock reports a time
157/// *before* the Unix epoch (e.g. a misconfigured RTC or a clock skewed
158/// backwards on cold boot). The `.unwrap_or_default()` silently clamps that
159/// case to a `Duration::ZERO`, so this function returns `0`. Callers treat
160/// `0` as "ancient", which makes the only observable effect under a broken
161/// clock be that stale entries become immediately eligible for garbage
162/// collection; no panic or error is propagated. This mirrors the silent-clamp
163/// behavior of the `current_time_ms` in `client::erased` so both clock
164/// sources stay consistent.
165#[inline]
166pub fn current_time_ms() -> u64 {
167    std::time::SystemTime::now()
168        .duration_since(std::time::UNIX_EPOCH)
169        .unwrap_or_default()
170        .as_millis() as u64
171}
172
173// ── Impl for MutationOptions integration ────────────────────────────────
174
175/// Allow `use_mutation((), cx)` to work with default options.
176impl From<()> for MutationOptions {
177    fn from((): ()) -> Self {
178        Self::default()
179    }
180}