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}