Skip to main content

gpui_query/client/
infinite_mutation_ops.rs

1//! Infinite query, mutation, and bulk operations on `QueryClient`.
2//!
3//! This module contains `impl QueryClient` methods for:
4//! - Infinite query resource management and lookups
5//! - Mutation registration and lookups
6//! - Bulk operations (invalidate/reset/remove/cancel) across all bucket types
7
8use std::any::TypeId;
9
10use gpui::{App, Entity};
11
12use crate::client::infinite_bucket::InfiniteQueryBucket;
13use crate::client::mutation_bucket::MutationBucket;
14use crate::core::{
15    CachePolicy, InfiniteQueryResource, MutationResource, QueryKey, QueryKeyFilter, RequestPolicy,
16};
17
18use super::QueryClient;
19
20impl QueryClient {
21    // ── Infinite query operations ───────────────────────────────────────
22
23    /// Get or create an infinite query resource for the given key and type pair.
24    pub fn infinite_resource<T: Clone + Send + Sync + 'static, E: Clone + Send + Sync + 'static>(
25        &mut self,
26        key: impl Into<QueryKey>,
27        cx: &mut App,
28    ) -> Entity<InfiniteQueryResource<T, E>> {
29        self.infinite_resource_with_policies::<T, E>(
30            key,
31            self.default_cache_policy,
32            self.default_request_policy,
33            cx,
34        )
35    }
36
37    /// Get or create an infinite query resource with explicit policies.
38    ///
39    /// Audit 3 fix (findings 3, 4): Graceful downcast recovery.
40    pub fn infinite_resource_with_policies<
41        T: Clone + Send + Sync + 'static,
42        E: Clone + Send + Sync + 'static,
43    >(
44        &mut self,
45        key: impl Into<QueryKey>,
46        cache_policy: CachePolicy,
47        request_policy: RequestPolicy,
48        cx: &mut App,
49    ) -> Entity<InfiniteQueryResource<T, E>> {
50        let type_id = TypeId::of::<(T, E)>();
51        let bucket = self
52            .infinite_buckets
53            .entry(type_id)
54            .or_insert_with(|| Box::new(InfiniteQueryBucket::<T, E>::new()));
55
56        // M4: single downcast via the shared helper (redundant TypeId
57        // pre-check dropped).
58        let typed = Self::infinite_bucket_or_recreate::<T, E>(bucket);
59        let entity = typed.get_or_create(key.into(), cache_policy, request_policy, cx);
60        // Audit fix CL1/#105: opportunistically run GC on this op.
61        self.maybe_opportunistic_gc(cx);
62        entity
63    }
64
65    /// Get a specific infinite query entity by key.
66    pub fn infinite_query<T: Clone + Send + Sync + 'static, E: Clone + Send + Sync + 'static>(
67        &self,
68        key: &QueryKey,
69    ) -> Option<Entity<InfiniteQueryResource<T, E>>> {
70        let type_id = TypeId::of::<(T, E)>();
71        self.infinite_buckets
72            .get(&type_id)
73            .and_then(|b| b.as_any().downcast_ref::<InfiniteQueryBucket<T, E>>())
74            .and_then(|b| b.get(key))
75    }
76
77    /// Use the infinite query bucket's co-located sequencer to generate a
78    /// `RequestId` for an infinite query key.
79    ///
80    /// Returns `None` if no bucket entry exists for the key. The sequencer is
81    /// advanced in-place so subsequent calls produce monotonically increasing IDs.
82    /// This is the infinite query equivalent of [`next_request_id_for_key`](Self::next_request_id_for_key).
83    ///
84    /// Audit 3 fix (findings 3, 4): Graceful downcast recovery.
85    pub fn next_request_id_for_infinite_key<
86        T: Clone + Send + Sync + 'static,
87        E: Clone + Send + Sync + 'static,
88    >(
89        &mut self,
90        key: &QueryKey,
91    ) -> Option<crate::core::RequestId> {
92        let type_id = TypeId::of::<(T, E)>();
93        let bucket = self.infinite_buckets.get_mut(&type_id)?;
94        // M4: single downcast via the shared helper (redundant TypeId
95        // pre-check dropped).
96        let typed = Self::infinite_bucket_or_recreate::<T, E>(bucket);
97        typed.sequencer_mut(key).map(|seq| seq.next_request())
98    }
99
100    /// Get all infinite query entities of a given type pair.
101    pub fn all_infinite_queries<
102        T: Clone + Send + Sync + 'static,
103        E: Clone + Send + Sync + 'static,
104    >(
105        &self,
106    ) -> Vec<Entity<InfiniteQueryResource<T, E>>> {
107        let type_id = TypeId::of::<(T, E)>();
108        self.infinite_buckets
109            .get(&type_id)
110            .and_then(|b| b.as_any().downcast_ref::<InfiniteQueryBucket<T, E>>())
111            .map(|b| b.all_entities())
112            .unwrap_or_default()
113    }
114
115    // ── Mutation operations ─────────────────────────────────────────────
116
117    /// Register a mutation entity.
118    ///
119    /// Audit 3 fix (findings 3, 4): Graceful downcast recovery.
120    pub fn register_mutation<
121        V: Clone + Send + Sync + 'static,
122        T: Clone + Send + Sync + 'static,
123        E: Clone + Send + Sync + 'static,
124    >(
125        &mut self,
126        entity: &Entity<MutationResource<V, T, E>>,
127        cx: &App,
128    ) {
129        let type_id = TypeId::of::<(V, T, E)>();
130        let bucket = self
131            .mutation_buckets
132            .entry(type_id)
133            .or_insert_with(|| Box::new(MutationBucket::<V, T, E>::new()));
134
135        // M6: cache now_ms once and thread it into `insert` (avoids a second
136        // `current_time_ms` syscall inside `insert`); the same value is reused
137        // by `maybe_opportunistic_gc` below.
138        let now_ms = crate::client::time::current_time_ms();
139        // M4: single downcast via the shared helper (redundant TypeId
140        // pre-check dropped).
141        let typed = Self::mutation_bucket_or_recreate::<V, T, E>(bucket);
142        typed.insert(entity, now_ms, cx);
143        // Audit fix CL1/#105: opportunistically run GC on this op so
144        // completed mutations are eventually evicted without manual gc() calls.
145        self.maybe_opportunistic_gc(cx);
146    }
147
148    /// Get all mutation entities of a given type triple.
149    pub fn all_mutations<
150        V: Clone + Send + Sync + 'static,
151        T: Clone + Send + Sync + 'static,
152        E: Clone + Send + Sync + 'static,
153    >(
154        &self,
155    ) -> Vec<Entity<MutationResource<V, T, E>>> {
156        let type_id = TypeId::of::<(V, T, E)>();
157        self.mutation_buckets
158            .get(&type_id)
159            .and_then(|b| b.as_any().downcast_ref::<MutationBucket<V, T, E>>())
160            .map(|b| b.all_entities())
161            .unwrap_or_default()
162    }
163
164    // ── Bulk operations ─────────────────────────────────────────────────
165
166    /// Apply an operation `f` to every query bucket (regular + infinite).
167    /// **L10**: extracted to kill the 4x duplicated
168    /// `for buckets … for infinite_buckets …` pair in the bulk-op methods
169    /// below. `f` is called once per regular bucket (as `Left`) and once per
170    /// infinite bucket (as `Right`); callers match on the side to invoke the
171    /// correct trait method.
172    fn for_each_query_bucket_mut<F>(&mut self, mut f: F)
173    where
174        F: FnMut(EitherBucket<'_>),
175    {
176        for bucket in self.buckets.values_mut() {
177            f(EitherBucket::Query(bucket.as_mut()));
178        }
179        for bucket in self.infinite_buckets.values_mut() {
180            f(EitherBucket::Infinite(bucket.as_mut()));
181        }
182    }
183
184    /// Invalidate queries matching the filter.
185    ///
186    /// Uses collect-then-update pattern to avoid nested entity borrows.
187    pub fn invalidate_queries(&mut self, filter: &QueryKeyFilter, cx: &mut App) {
188        self.for_each_query_bucket_mut(|b| match b {
189            EitherBucket::Query(b) => b.invalidate_matching(filter, cx),
190            EitherBucket::Infinite(b) => b.invalidate_matching(filter, cx),
191        });
192    }
193
194    /// Reset queries matching the filter.
195    pub fn reset_queries(&mut self, filter: &QueryKeyFilter, cx: &mut App) {
196        self.for_each_query_bucket_mut(|b| match b {
197            EitherBucket::Query(b) => b.reset_matching(filter, cx),
198            EitherBucket::Infinite(b) => b.reset_matching(filter, cx),
199        });
200    }
201
202    /// Remove queries matching the filter.
203    pub fn remove_queries(&mut self, filter: &QueryKeyFilter) {
204        self.for_each_query_bucket_mut(|b| match b {
205            EitherBucket::Query(b) => b.remove_matching(filter),
206            EitherBucket::Infinite(b) => b.remove_matching(filter),
207        });
208    }
209
210    /// Cancel in-flight requests matching the filter (Audit 3, Finding 5).
211    ///
212    /// Iterates all query and infinite query buckets, finds resources with active
213    /// requests, and cancels them with a [`QueryError::cancelled`] error. This is
214    /// essential for cleanup when navigating away from a page or when bulk
215    /// cancellation is needed.
216    ///
217    /// Equivalent to TanStack Query's `queryClient.cancelQueries()`. Individual
218    /// `QueryResource::cancel()` exists but this is the bulk cancellation method
219    /// on the client.
220    pub fn cancel_queries(&mut self, filter: &QueryKeyFilter, cx: &mut App) {
221        self.for_each_query_bucket_mut(|b| match b {
222            EitherBucket::Query(b) => b.cancel_matching(filter, cx),
223            EitherBucket::Infinite(b) => b.cancel_matching(filter, cx),
224        });
225    }
226
227    // ── Erased-bucket recovery helpers (M4) ──────────────────────────────
228    //
229    // Mirrors `QueryClient::bucket_or_recreate` in `mod.rs` for the infinite
230    // and mutation maps: downcast once (the redundant TypeId pre-check is
231    // dropped — `downcast_mut` checks it internally) and recreate the bucket
232    // in place on the (impossible) mismatch. Kills the 3x duplicated recovery
233    // blocks that lived in `infinite_resource_with_policies`,
234    // `next_request_id_for_infinite_key`, and `register_mutation`.
235
236    fn infinite_bucket_or_recreate<
237        T: Clone + Send + Sync + 'static,
238        E: Clone + Send + Sync + 'static,
239    >(
240        bucket: &mut Box<dyn super::erased::ErasedInfiniteBucket>,
241    ) -> &mut InfiniteQueryBucket<T, E> {
242        if bucket
243            .as_any_mut()
244            .downcast_mut::<InfiniteQueryBucket<T, E>>()
245            .is_none()
246        {
247            eprintln!(
248                "QueryClient: type mismatch in infinite bucket downcast for {}. \
249                 Replacing with a fresh bucket.",
250                std::any::type_name::<(T, E)>()
251            );
252            *bucket = Box::new(InfiniteQueryBucket::<T, E>::new());
253        }
254        bucket
255            .as_any_mut()
256            .downcast_mut::<InfiniteQueryBucket<T, E>>()
257            .expect("InfiniteQueryBucket downcast succeeds after infinite_bucket_or_recreate")
258    }
259
260    fn mutation_bucket_or_recreate<
261        V: Clone + Send + Sync + 'static,
262        T: Clone + Send + Sync + 'static,
263        E: Clone + Send + Sync + 'static,
264    >(
265        bucket: &mut Box<dyn super::erased::ErasedMutationBucket>,
266    ) -> &mut MutationBucket<V, T, E> {
267        if bucket
268            .as_any_mut()
269            .downcast_mut::<MutationBucket<V, T, E>>()
270            .is_none()
271        {
272            eprintln!(
273                "QueryClient: type mismatch in mutation bucket downcast for {}. \
274                 Replacing with a fresh bucket.",
275                std::any::type_name::<(V, T, E)>()
276            );
277            *bucket = Box::new(MutationBucket::<V, T, E>::new());
278        }
279        bucket
280            .as_any_mut()
281            .downcast_mut::<MutationBucket<V, T, E>>()
282            .expect("MutationBucket downcast succeeds after mutation_bucket_or_recreate")
283    }
284}
285
286/// One side of a query bucket iteration (L10).
287enum EitherBucket<'a> {
288    Query(&'a mut dyn crate::client::erased::ErasedBucket),
289    Infinite(&'a mut dyn crate::client::erased::ErasedInfiniteBucket),
290}