Skip to content

Commit dade483

Browse files
committed
refactor: Move strategy state management out of Query struct
- Remove strategy-specific fields from Query struct (debounce_source_id, throttle_last_exec_time, throttle_trailing_source_id) - Refactor strategies to manage their own state using closure-captured Rc<RefCell<>> - Debounce strategy now encapsulates its own SourceId state - Throttle strategy now encapsulates its own last_exec_time and trailing_source_id state - Query struct is cleaner and doesn't know about strategy implementation details - Strategies are fully independent and composable - API remains unchanged and ergonomic - Automatic cleanup when strategies complete or are superseded
1 parent 77285ea commit dade483

1 file changed

Lines changed: 23 additions & 33 deletions

File tree

src/query/mod.rs

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,6 @@ pub struct QueryInner<T> {
2727

2828
retry_strategy: Option<Box<dyn Fn(u32) -> Option<Duration>>>,
2929
retry_count: u32,
30-
31-
/// Shared state for strategy implementations
32-
/// Source ID for pending debounce timer
33-
debounce_source_id: Option<glib::SourceId>,
34-
/// Instant of the last throttled fetch execution
35-
last_throttle_time: Option<Instant>,
36-
/// Source ID for pending trailing throttle timer
37-
throttle_trailing_source_id: Option<glib::SourceId>,
3830
}
3931

4032
impl<T> QueryInner<T> {
@@ -60,9 +52,6 @@ impl<T> QueryInner<T> {
6052
timeout,
6153
retry_strategy: None,
6254
retry_count: 0,
63-
debounce_source_id: None,
64-
last_throttle_time: None,
65-
throttle_trailing_source_id: None,
6655
}
6756
}
6857

@@ -190,16 +179,6 @@ impl<T> Drop for Query<T> {
190179
source_id.remove();
191180
}
192181

193-
// Remove any pending debounce timer
194-
if let Some(source_id) = self.inner.borrow_mut().debounce_source_id.take() {
195-
source_id.remove();
196-
}
197-
198-
// Remove any pending throttle trailing timer
199-
if let Some(source_id) = self.inner.borrow_mut().throttle_trailing_source_id.take() {
200-
source_id.remove();
201-
}
202-
203182
// Abort any active fetch task to ensure cleanup
204183
if let Some(handle) = self.inner.borrow_mut().fetch_task_handle.take() {
205184
debug!(resource_key = %self.inner.borrow().key, "Dropping last reference to Query, aborting active fetch task");
@@ -243,84 +222,95 @@ where
243222
/// Strategy: Debounce fetch calls
244223
/// Waits for `duration` after the last call before executing.
245224
/// If another call arrives before the timer fires, the timer resets.
225+
///
226+
/// State is managed internally by the strategy and not stored in Query.
246227
pub fn debounce(duration: Duration) -> impl FnOnce(&Query<T>) {
228+
// Strategy state: managed by the closure itself
229+
let debounce_state: Rc<RefCell<Option<glib::SourceId>>> = Rc::new(RefCell::new(None));
230+
247231
move |query: &Query<T>| {
248232
let key = { query.inner.borrow().key.clone() };
249233

250234
// Cancel any existing debounce timer
251-
if let Some(source_id) = query.inner.borrow_mut().debounce_source_id.take() {
235+
if let Some(source_id) = debounce_state.borrow_mut().take() {
252236
debug!(resource_key = %key, "Cancelling previous debounce timer");
253237
source_id.remove();
254238
}
255239

256240
let weak = Rc::downgrade(&query.inner);
241+
let state_for_callback = debounce_state.clone();
257242
let source_id = glib::timeout_add_local_once(duration, move || {
258243
if let Some(inner) = weak.upgrade() {
259244
let query = Query { inner };
260245
let key = { query.inner.borrow().key.clone() };
261246
debug!(resource_key = %key, "Debounce timer fired, executing fetch");
262247
// Clear the source_id since timer has fired
263-
query.inner.borrow_mut().debounce_source_id = None;
248+
*state_for_callback.borrow_mut() = None;
264249
query.fetch();
265250
}
266251
});
267252

268253
debug!(resource_key = %key, duration_ms = duration.as_millis(), "Scheduled debounced fetch");
269-
query.inner.borrow_mut().debounce_source_id = Some(source_id);
254+
*debounce_state.borrow_mut() = Some(source_id);
270255
}
271256
}
272257

273258
/// Strategy: Throttle fetch calls
274259
/// Executes at most once per `interval`.
275260
/// If `trailing` is true, a trailing fetch will be scheduled after the interval
276261
/// if calls arrived during the throttle period.
262+
///
263+
/// State is managed internally by the strategy and not stored in Query.
277264
pub fn throttle(interval: Duration, trailing: bool) -> impl FnOnce(&Query<T>) {
265+
// Strategy state: managed by the closure itself
266+
let throttle_state: Rc<RefCell<(Option<Instant>, Option<glib::SourceId>)>> =
267+
Rc::new(RefCell::new((None, None)));
268+
278269
move |query: &Query<T>| {
279270
let key = { query.inner.borrow().key.clone() };
280271
let now = Instant::now();
281272

282-
let last_throttle_time = { query.inner.borrow().last_throttle_time };
273+
let last_throttle_time = { throttle_state.borrow().0 };
274+
283275
let should_fetch = match last_throttle_time {
284276
None => true,
285277
Some(last_time) => now.duration_since(last_time) >= interval,
286278
};
287279

288280
if should_fetch {
289281
// Cancel any pending trailing timer since we're fetching now
290-
if let Some(source_id) = query.inner.borrow_mut().throttle_trailing_source_id.take()
291-
{
282+
if let Some(source_id) = throttle_state.borrow_mut().1.take() {
292283
debug!(resource_key = %key, "Cancelling trailing throttle timer (immediate fetch)");
293284
source_id.remove();
294285
}
295286

296287
debug!(resource_key = %key, "Throttle allows fetch, executing immediately");
297-
query.inner.borrow_mut().last_throttle_time = Some(now);
288+
*throttle_state.borrow_mut() = (Some(now), None);
298289
query.fetch();
299290
} else if trailing {
300291
// Schedule a trailing fetch if not already scheduled
301-
let has_pending_trailing =
302-
{ query.inner.borrow().throttle_trailing_source_id.is_some() };
292+
let has_pending_trailing = { throttle_state.borrow().1.is_some() };
303293

304294
if !has_pending_trailing {
305295
let remaining = interval
306296
.checked_sub(now.duration_since(last_throttle_time.unwrap()))
307297
.unwrap_or(Duration::ZERO);
308298

309299
let weak = Rc::downgrade(&query.inner);
300+
let state_for_callback = throttle_state.clone();
310301
let source_id = glib::timeout_add_local_once(remaining, move || {
311302
if let Some(inner) = weak.upgrade() {
312303
let query = Query { inner };
313304
let key = { query.inner.borrow().key.clone() };
314305
debug!(resource_key = %key, "Trailing throttle timer fired, executing fetch");
315306
// Clear the source_id and update throttle time
316-
query.inner.borrow_mut().throttle_trailing_source_id = None;
317-
query.inner.borrow_mut().last_throttle_time = Some(Instant::now());
307+
*state_for_callback.borrow_mut() = (Some(Instant::now()), None);
318308
query.fetch();
319309
}
320310
});
321311

322312
debug!(resource_key = %key, remaining_ms = remaining.as_millis(), "Scheduled trailing throttle fetch");
323-
query.inner.borrow_mut().throttle_trailing_source_id = Some(source_id);
313+
throttle_state.borrow_mut().1 = Some(source_id);
324314
} else {
325315
debug!(resource_key = %key, "Throttled: trailing timer already pending");
326316
}

0 commit comments

Comments
 (0)