Skip to main content

gpui_query/hook/
use_query_select.rs

1//! The `use_query_select` hook — combines `use_query` with a [`SelectTransform`].
2//!
3//! TanStack Query's `select` option transforms cached data into a derived shape,
4//! re-running only when data changes. This module provides the same pattern for
5//! gpui-query: it wraps a [`QueryResource`] with a [`MappedQueryResource`]
6//! entity that applies the transform on each observer notification.
7//!
8//! # Why a separate hook?
9//!
10//! Rust's type system requires knowing `T` (source) and `U` (output) at compile
11//! time. Since `QueryOptions` is not generic over a transform output type, the
12//! `select` field cannot live on `QueryOptions` without making the entire options
13//! struct generic. Instead, [`use_query_select`] is a standalone hook that accepts
14//! the transform as a separate parameter and returns a
15//! `MappedQueryResource<T, U, E>` entity.
16//!
17//! # Usage
18//!
19//! ```no_run
20//! use gpui_query::hook::{use_query_select, QueryOptions};
21//! use gpui_query::core::SelectTransform;
22//! # #[derive(Clone, PartialEq)]
23//! # struct User;
24//! # #[derive(Clone, Debug)]
25//! # struct MyError;
26//!
27//! struct UserCountView {
28//!     mapped: gpui::Entity<gpui_query::core::MappedQueryResource<Vec<User>, usize, MyError>>,
29//!     _subs: (gpui::Subscription, gpui::Subscription),
30//! }
31//!
32//! impl UserCountView {
33//!     fn new(cx: &mut gpui::Context<Self>) -> Self {
34//!         let count_transform = SelectTransform::new(|users: &Vec<User>| users.len());
35//!         let (mapped, query_entity, subs) = use_query_select(
36//!             QueryOptions::new("users"),
37//!             count_transform,
38//!             |signal| async move {
39//!                 // Your async fetcher here
40//!                 Ok(vec![])
41//!             },
42//!             cx,
43//!         );
44//!         Self { mapped, _subs: subs }
45//!     }
46//! }
47//! ```
48
49use std::sync::Arc;
50
51use gpui::{AppContext as _, Context, Entity, Subscription};
52
53use crate::core::{MappedQueryResource, QueryResource, SelectTransform};
54
55use super::{QueryOptions, use_query};
56
57/// The result of [`use_query_select`]: the projected view entity, the
58/// underlying query entity, and the pair of subscriptions that keep both
59/// observations alive.
60///
61/// Introduced as a type alias (audit #96) to satisfy `clippy::type_complexity`
62/// on the public hook signature and to give callers a name to reference.
63pub type QuerySelectResult<T, U, E> = (
64    Entity<MappedQueryResource<T, U, E>>,
65    Entity<QueryResource<T, E>>,
66    (Subscription, Subscription),
67);
68
69/// Subscribe to a query and project its data through a [`SelectTransform`].
70///
71/// This is the "select" integration point for the hook layer (audit #3, HIGH
72/// finding). It:
73///
74/// 1. Calls [`use_query`] to create/subscribe to the underlying `QueryResource`.
75/// 2. Creates a `MappedQueryResource<T, U, E>` entity seeded with the current
76///    source data.
77/// 3. Observes the source entity so that every time it changes, the mapped
78///    resource's source data is updated from the fresh `QueryResource::data()`.
79///    The transform itself is applied lazily when
80///    [`MappedQueryResource::data()`] is called.
81///
82/// # Returns
83///
84/// A tuple of:
85/// - `Entity<MappedQueryResource<T, U, E>>` — the projected view entity
86/// - `Entity<QueryResource<T, E>>` — the underlying query entity (for status,
87///   error, refetch, etc.)
88/// - `(Subscription, Subscription)` — the query subscription and the mapped
89///   observer subscription. Store both to keep observations alive.
90///
91/// # Transform cost
92///
93/// The transform closure runs every time `mapped.data()` is called (no output
94/// cache). For expensive transforms, cache the result:
95///
96/// ```no_run
97/// use gpui_query::core::{MappedQueryResource, SelectTransform};
98/// # fn _doc(mapped: &gpui::Entity<MappedQueryResource<Vec<String>, usize, ()>>, cx: &gpui::App) {
99///
100/// let count = mapped.read(cx).data(); // transform runs once
101/// // reuse `count` below
102/// # }
103/// ```
104pub fn use_query_select<T, U, E, C, F, Fut>(
105    options: impl Into<QueryOptions>,
106    transform: SelectTransform<T, U>,
107    fetcher: F,
108    cx: &mut Context<C>,
109) -> QuerySelectResult<T, U, E>
110where
111    T: Clone + PartialEq + Send + Sync + 'static,
112    U: 'static,
113    E: Clone + Send + Sync + std::fmt::Debug + 'static,
114    C: 'static,
115    F: Fn(crate::core::QuerySignal) -> Fut + Send + 'static,
116    Fut: std::future::Future<Output = Result<T, E>> + Send + 'static,
117{
118    // Step 1: Create the underlying query entity and start the fetch.
119    let (query_entity, query_subscription) = use_query(options, fetcher, cx);
120
121    // Step 2: Seed the mapped resource with whatever data the query has now.
122    // The source `QueryResource` owns `T` by value and only lends `&T`, so the
123    // initial seed clones `T` once into an `Arc<T>` (audit #20). Subsequent
124    // updates (Step 3) only re-clone `T` when the content has actually changed.
125    let initial_data: Option<Arc<T>> =
126        query_entity.read_with(cx, |r, _| r.data().map(|d| Arc::new(d.clone())));
127    let mapped = MappedQueryResource::new(initial_data, transform);
128    let mapped_entity = cx.new(|_| mapped);
129
130    // Step 3: Observe the query entity so the mapped resource stays in sync.
131    // Every time the query entity is updated (fetch completes, refetch, cache
132    // invalidation, etc.), we read the fresh source data once, compare it
133    // against the cached source, and only notify + re-store when it changed.
134    //
135    // Audit H1: the previous version cloned `T` into a fresh `Arc<T>` on
136    // EVERY notification (even the common case where data was unchanged) just
137    // to drive the `PartialEq` comparison. This version avoids that O(|T|)
138    // clone on unchanged notifications:
139    //   1. Clone the cached `Arc<T>` out of the mapped resource first via
140    //      `source_arc()` — a cheap refcount bump, no `T` clone. The mapped
141    //      borrow ends with that call (audit #115 preserved: no nested borrow).
142    //   2. Read the fresh `&T` straight from the source entity and compare
143    //      `&T` vs `&T` without cloning `T`.
144    //   3. Only when the content actually changed do we clone `T` into an
145    //      `Arc<T>` to hand to `update_source`, exactly as before.
146    // Net: unchanged notifications (the common case) pay one cheap `Arc::clone`
147    // instead of a full `T` clone + allocation; changed notifications behave
148    // identically (same `update_source` + `notify`).
149    //
150    // Audit fix #4 / #20: source data is still stored as `Option<Arc<T>>`, so
151    // `MappedQueryResource` clones (derived views, entity cloning) remain cheap
152    // `Arc::clone`s and storage stays shared.
153    let mapped_weak = mapped_entity.downgrade();
154    let mapped_subscription = cx.observe(&query_entity, move |_, entity, cx| {
155        if let Some(mapped) = mapped_weak.upgrade() {
156            // Audit fix #115 / H1 step 1: Read the cached source `Arc<T>` out
157            // of the mapped resource FIRST, as an owned value (cheap refcount
158            // bump via `source_arc`). The mapped borrow ends here, so the
159            // `entity.read(cx)` below does NOT nest inside it.
160            let cached: Option<Arc<T>> = mapped.read_with(cx, |m, _| m.source_arc());
161
162            // H1 step 2: Compare cached `&T` vs fresh `&T` WITHOUT cloning T.
163            // `cached` is owned, so no nested borrow is taken on `mapped`.
164            let changed = entity.read_with(cx, |r, _| match (&cached, r.data()) {
165                (Some(c), Some(fresh)) => c.as_ref() != fresh,
166                (None, None) => false,
167                _ => true,
168            });
169
170            if changed {
171                // H1 step 3: Only now clone `T` into an `Arc<T>` for the
172                // update (the source `QueryResource` owns `T` by value and only
173                // lends `&T`, so this single clone is unavoidable on change).
174                let fresh: Option<Arc<T>> = entity.read(cx).data().map(|d| Arc::new(d.clone()));
175                // Audit fix #116: Notify after updating the mapped source so
176                // third-party observers of the mapped entity (not just the
177                // primary caller, which already re-renders via the query
178                // subscription) see the derived change. Safe and correct.
179                mapped.update(cx, |m, cx2| {
180                    m.update_source(fresh);
181                    cx2.notify();
182                });
183            }
184        }
185    });
186
187    (
188        mapped_entity,
189        query_entity,
190        (query_subscription, mapped_subscription),
191    )
192}