The debounce state machine for a healthy watcher. Wakes on events, sleeps until the quiet deadline or the hard cap (whichever comes first), then drains and syncs. No busy polling.
(inner: &Arc<GitWatcherInner>, state: &Arc<WatchState>, common: &Path)
| 499 | /// until the quiet deadline or the hard cap (whichever comes first), then |
| 500 | /// drains and syncs. No busy polling. |
| 501 | async fn debounce_loop(inner: &Arc<GitWatcherInner>, state: &Arc<WatchState>, common: &Path) { |
| 502 | let quiet = Duration::from_millis(inner.config.watch_debounce_ms); |
| 503 | let max_delay = Duration::from_millis(inner.config.watch_max_delay_ms); |
| 504 | |
| 505 | #[cfg(test)] |
| 506 | state.entered_debounce.notify_one(); |
| 507 | |
| 508 | loop { |
| 509 | // Sleep until the first event arrives. |
| 510 | state.wake.notified().await; |
| 511 | state.health.beat(); |
| 512 | |
| 513 | // Coalesce: keep extending the quiet window until it settles or we hit |
| 514 | // the hard cap. If a rebase/merge is mid-flight, HOLD (keep waiting) |
| 515 | // until the markers disappear so we sync exactly once, after. |
| 516 | loop { |
| 517 | let (first, last) = { |
| 518 | let dirty = state.dirty.lock().await; |
| 519 | (dirty.first_event, dirty.last_event) |
| 520 | }; |
| 521 | let now = Instant::now(); |
| 522 | let quiet_deadline = last.map(|l| l + quiet); |
| 523 | let hard_deadline = first.map(|f| f + max_delay); |
| 524 | |
| 525 | // If an operation is in flight, do not fire yet — wait for the next |
| 526 | // event (marker removal wakes us) or a short recheck tick. |
| 527 | if operation_in_flight(common) { |
| 528 | tokio::select! { |
| 529 | () = state.wake.notified() => { state.health.beat(); continue; } |
| 530 | () = tokio::time::sleep(Duration::from_secs(1)) => { continue; } |
| 531 | } |
| 532 | } |
| 533 | |
| 534 | // Fire when the quiet window elapsed, but never later than the cap. |
| 535 | let fire_at = match (quiet_deadline, hard_deadline) { |
| 536 | (Some(q), Some(h)) => q.min(h), |
| 537 | (Some(q), None) => q, |
| 538 | (None, Some(h)) => h, |
| 539 | (None, None) => break, // nothing pending; back to outer wait |
| 540 | }; |
| 541 | if now >= fire_at { |
| 542 | break; |
| 543 | } |
| 544 | let sleep_for = fire_at - now; |
| 545 | tokio::select! { |
| 546 | () = state.wake.notified() => { state.health.beat(); } |
| 547 | () = tokio::time::sleep(sleep_for) => {} |
| 548 | } |
| 549 | } |
| 550 | |
| 551 | // Drain and execute exactly one coalesced sync pass. |
| 552 | let plan = { |
| 553 | let mut dirty = state.dirty.lock().await; |
| 554 | dirty.take() |
| 555 | }; |
| 556 | if !plan.is_empty() { |
| 557 | execute_plan(inner, state, common, plan).await; |
| 558 | } |
no test coverage detected