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>>;