Skip to main content

gpui_query/client/
observer.rs

1//! Query observer for reactive state tracking.
2//!
3//! **v2 improvements**:
4//! - `observe()` returns `Option<Subscription>` instead of panicking on dropped entity
5//! - Status deduplication to avoid unnecessary `cx.notify()` calls
6//!
7//! **Audit L8**: the three formerly-structurally-identical observer types
8//! (`QueryObserver`, `InfiniteQueryObserver`, `MutationObserver`) are now a
9//! single generic [`Observer<R>`] plus type aliases. They differed only in
10//! their entity type and status type (`QueryStatus` vs `MutationStatus`); the
11//! observe logic (dedup `cx.notify()` via a `Cell<Option<S>>`, fall back to
12//! unconditional notify) is shared by the one generic impl.
13
14use std::cell::Cell;
15
16use gpui::{Context, Entity, Subscription};
17
18use crate::core::{
19    InfiniteQueryResource, MutationResource, MutationStatus, QueryResource, QueryStatus,
20};
21
22/// Bridges a resource type to its status for the generic [`Observer`].
23///
24/// Each resource exposes its status via an *inherent* `status()` method, which
25/// cannot be called generically without a trait; this trait (pub(crate), not
26/// part of the public API) surfaces it with an associated `Status` type so
27/// [`Observer<R>`] can dedup notifications for any resource kind.
28pub trait ObservableResource {
29    type Status: PartialEq + Copy + 'static;
30
31    fn observable_status(&self) -> Self::Status;
32}
33
34impl<T: 'static, E: 'static> ObservableResource for QueryResource<T, E> {
35    type Status = QueryStatus;
36
37    fn observable_status(&self) -> QueryStatus {
38        self.status()
39    }
40}
41
42impl<T: 'static, E: 'static> ObservableResource for InfiniteQueryResource<T, E> {
43    type Status = QueryStatus;
44
45    fn observable_status(&self) -> QueryStatus {
46        self.status()
47    }
48}
49
50impl<V: 'static, T: 'static, E: 'static> ObservableResource for MutationResource<V, T, E> {
51    type Status = MutationStatus;
52
53    fn observable_status(&self) -> MutationStatus {
54        self.status()
55    }
56}
57
58/// Configuration for a query observer.
59#[derive(Clone, Debug)]
60pub struct ObserverConfig {
61    /// Only notify when status changes (dedup re-renders).
62    pub notify_on_status_change_only: bool,
63}
64
65impl Default for ObserverConfig {
66    fn default() -> Self {
67        Self {
68            notify_on_status_change_only: true,
69        }
70    }
71}
72
73/// Observes a resource and triggers re-renders only on status changes.
74///
75/// In v2, the observer only calls `cx.notify()` when the status actually
76/// changes, preventing excessive re-renders from intermediate state updates
77/// like retry count increments.
78///
79/// This is a single generic implementation shared by every resource kind
80/// (audit L8). Use the [`QueryObserver`] / [`InfiniteQueryObserver`] /
81/// [`MutationObserver`] type aliases for the concrete kinds.
82///
83/// This is also the fix for audit findings #1/#11: the raw `cx.observe` in
84/// `use_mutation` unconditionally called `cx.notify()` on every entity
85/// mutation, causing 2-3 re-renders per retry attempt. By tracking the last
86/// status and only notifying on change, `increment_retry()` / `prepare_retry()`
87/// calls (which don't change status — it stays Loading) no longer trigger
88/// re-renders.
89pub struct Observer<R> {
90    entity: gpui::WeakEntity<R>,
91    config: ObserverConfig,
92}
93
94impl<R: ObservableResource + 'static> Observer<R> {
95    /// Create a new observer for the given entity.
96    pub fn new(entity: &Entity<R>) -> Self {
97        Self {
98            entity: entity.downgrade(),
99            config: ObserverConfig::default(),
100        }
101    }
102
103    /// Set the observer configuration.
104    pub fn with_config(mut self, config: ObserverConfig) -> Self {
105        self.config = config;
106        self
107    }
108
109    /// Start observing the entity. Returns `None` if the entity was already dropped.
110    ///
111    /// **v2 fix**: Returns `Option<Subscription>` instead of panicking.
112    ///
113    /// **Audit #71**: takes `&self` (was `&mut self`) — the body only reads the
114    /// weak entity handle and the `Copy` config flag, so no interior mutation
115    /// is required. `&mut` callers coerce to `&` with no ripple.
116    pub fn observe<W: 'static>(&self, cx: &mut Context<W>) -> Option<Subscription> {
117        let upgraded = self.entity.upgrade()?;
118        let notify_on_change = self.config.notify_on_status_change_only;
119        let last_status: Cell<Option<R::Status>> = Cell::new(None);
120
121        let subscription = cx.observe(&upgraded, move |_, entity, cx| {
122            let current_status = entity.read(cx).observable_status();
123            if notify_on_change {
124                let previous = last_status.get();
125                if previous != Some(current_status) {
126                    last_status.set(Some(current_status));
127                    cx.notify();
128                }
129            } else {
130                cx.notify();
131            }
132        });
133
134        Some(subscription)
135    }
136}
137
138/// Observer for a [`QueryResource`] (status type [`QueryStatus`]).
139pub type QueryObserver<T, E> = Observer<QueryResource<T, E>>;
140
141/// Observer for an [`InfiniteQueryResource`] (status type [`QueryStatus`]).
142pub type InfiniteQueryObserver<T, E> = Observer<InfiniteQueryResource<T, E>>;
143
144/// Observer for a [`MutationResource`] (status type [`MutationStatus`]).
145pub type MutationObserver<V, T, E> = Observer<MutationResource<V, T, E>>;