feat: push checkout #14
@@ -235,6 +235,35 @@ impl CheckoutsStore {
|
|||||||
self.refresh(cx);
|
self.refresh(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The checkout at `path` was just pushed to the remote.
|
||||||
|
///
|
||||||
|
/// Its ready-to-push status is obsolete. Drop it from the cached statuses
|
||||||
|
/// and notify observers right away, so the sidebar badge and the push
|
||||||
|
/// banner update immediately instead of waiting for the next background
|
||||||
|
/// pass, which re-scans and re-fetches the remote. The debounced refresh
|
||||||
|
/// reconciles the remaining checkouts of the repository afterwards.
|
||||||
|
pub fn checkout_pushed(&mut self, addr: &RepoAddr, path: &Path, cx: &mut Context<Self>) {
|
||||||
|
let mut removed = false;
|
||||||
|
|
||||||
|
if let Some(statuses) = self.push_statuses.get_mut(addr) {
|
||||||
|
let before = statuses.len();
|
||||||
|
statuses.retain(|status| status.path.as_path() != path);
|
||||||
|
removed = statuses.len() != before;
|
||||||
|
|
||||||
|
if removed && statuses.is_empty() {
|
||||||
|
self.push_statuses.remove(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if removed {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other checkouts of this repository still need re-deriving
|
||||||
|
// against the remote, now that the pushed refs landed there.
|
||||||
|
self.request_push_statuses(addr, cx);
|
||||||
|
}
|
||||||
|
|
||||||
/// The ready-to-push statuses of `addr`.
|
/// The ready-to-push statuses of `addr`.
|
||||||
///
|
///
|
||||||
/// Empty while none are known or nothing is unpushed.
|
/// Empty while none are known or nothing is unpushed.
|
||||||
@@ -402,7 +431,7 @@ impl CheckoutsStore {
|
|||||||
// Keep the statuses current while any repository panel is open.
|
// Keep the statuses current while any repository panel is open.
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
if poll && this.refresh.idle() {
|
if poll && this.refresh.idle() {
|
||||||
this.refresh.debounce();
|
this.refresh.poll();
|
||||||
|
|
||||||
// Open panels get the fast cadence.
|
// Open panels get the fast cadence.
|
||||||
// Each cycle fetches every watched checkout's remote.
|
// Each cycle fetches every watched checkout's remote.
|
||||||
@@ -414,7 +443,13 @@ impl CheckoutsStore {
|
|||||||
|
|
||||||
let task = cx.spawn(async move |this, cx| {
|
let task = cx.spawn(async move |this, cx| {
|
||||||
cx.background_executor().timer(delay).await;
|
cx.background_executor().timer(delay).await;
|
||||||
this.update(cx, |this, cx| this.run_refresh(cx))
|
this.update(cx, |this, cx| {
|
||||||
|
// A request that arrived while the poll was pending
|
||||||
|
// superseded it with its own debounce; skip the stale poll.
|
||||||
|
if this.refresh.take_poll() {
|
||||||
|
this.run_refresh(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.push_task(task);
|
this.push_task(task);
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
/// [`crate::RepoStore`], [`crate::RepoListStore`] and [`crate::CheckoutsStore`]
|
/// [`crate::RepoStore`], [`crate::RepoListStore`] and [`crate::CheckoutsStore`]
|
||||||
/// re-query their inputs on a debounce timer with the same policy:
|
/// re-query their inputs on a debounce timer with the same policy:
|
||||||
/// a request arriving while a run is in flight is folded into a follow-up run,
|
/// a request arriving while a run is in flight is folded into a follow-up run,
|
||||||
/// a request arriving while the debounce timer is pending is dropped by it.
|
/// a request arriving while a request debounce is pending is dropped by it.
|
||||||
|
///
|
||||||
|
/// A slow poll cycle (`poll`) is different: it keeps a store's derived data
|
||||||
|
/// fresh while nothing is happening, but it must never delay a real request.
|
||||||
|
/// A request arriving while a poll is pending supersedes the poll with its own
|
||||||
|
/// short debounce, so external events (a push landing, a settings change)
|
||||||
|
/// propagate promptly instead of waiting out the poll interval.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct RefreshGate {
|
pub struct RefreshGate {
|
||||||
/// A run is in flight.
|
/// A run is in flight.
|
||||||
@@ -12,6 +18,10 @@ pub struct RefreshGate {
|
|||||||
dirty: bool,
|
dirty: bool,
|
||||||
/// The debounce timer is pending.
|
/// The debounce timer is pending.
|
||||||
debouncing: bool,
|
debouncing: bool,
|
||||||
|
/// The pending debounce is a poll cycle, not a request.
|
||||||
|
///
|
||||||
|
/// Polls wait for a quiet moment; requests supersede them.
|
||||||
|
poll: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a refresh request decided.
|
/// What a refresh request decided.
|
||||||
@@ -41,28 +51,47 @@ impl RefreshGate {
|
|||||||
|
|
||||||
/// A new refresh request arrived.
|
/// A new refresh request arrived.
|
||||||
///
|
///
|
||||||
/// Folded into a follow-up run while one is in flight, dropped while the
|
/// Folded into a follow-up run while one is in flight or a request debounce
|
||||||
/// debounce timer is pending, otherwise starts the timer.
|
/// is pending, superseding a slow poll with the request's own debounce,
|
||||||
|
/// otherwise starts the debounce timer.
|
||||||
pub fn request(&mut self) -> RefreshRequest {
|
pub fn request(&mut self) -> RefreshRequest {
|
||||||
if self.running {
|
if self.running {
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
RefreshRequest::Fold
|
RefreshRequest::Fold
|
||||||
} else if self.debouncing {
|
} else if self.debouncing && !self.poll {
|
||||||
RefreshRequest::Fold
|
RefreshRequest::Fold
|
||||||
} else {
|
} else {
|
||||||
|
// A request supersedes a pending poll: start the short debounce.
|
||||||
self.debouncing = true;
|
self.debouncing = true;
|
||||||
|
self.poll = false;
|
||||||
RefreshRequest::Schedule
|
RefreshRequest::Schedule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A timer was started without a request, e.g. a poll cycle.
|
/// A slow poll timer was started without a request.
|
||||||
pub fn debounce(&mut self) {
|
pub fn poll(&mut self) {
|
||||||
self.debouncing = true;
|
self.debouncing = true;
|
||||||
|
self.poll = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A poll timer fired. Whether it is still the scheduled pass and may run.
|
||||||
|
///
|
||||||
|
/// A request that arrived while the poll was pending superseded it with its
|
||||||
|
/// own debounce, so the stale poll timer is skipped.
|
||||||
|
pub fn take_poll(&mut self) -> bool {
|
||||||
|
if self.debouncing && self.poll {
|
||||||
|
self.debouncing = false;
|
||||||
|
self.poll = false;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The debounce timer fired and the run starts now.
|
/// The debounce timer fired and the run starts now.
|
||||||
pub fn begin(&mut self) {
|
pub fn begin(&mut self) {
|
||||||
self.debouncing = false;
|
self.debouncing = false;
|
||||||
|
self.poll = false;
|
||||||
self.running = true;
|
self.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,3 +106,83 @@ impl RefreshGate {
|
|||||||
self.running = false;
|
self.running = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{RefreshGate, RefreshRequest};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_starts_the_debounce_when_idle() {
|
||||||
|
let mut gate = RefreshGate::default();
|
||||||
|
assert_eq!(gate.request(), RefreshRequest::Schedule);
|
||||||
|
assert!(gate.debouncing());
|
||||||
|
assert!(!gate.idle());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_folds_into_a_request_debounce() {
|
||||||
|
let mut gate = RefreshGate::default();
|
||||||
|
gate.request();
|
||||||
|
assert_eq!(gate.request(), RefreshRequest::Fold);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_folds_into_a_running_run_and_runs_again() {
|
||||||
|
let mut gate = RefreshGate::default();
|
||||||
|
gate.request();
|
||||||
|
gate.begin();
|
||||||
|
assert!(gate.running());
|
||||||
|
assert_eq!(gate.request(), RefreshRequest::Fold);
|
||||||
|
assert!(gate.finish());
|
||||||
|
assert_eq!(gate.request(), RefreshRequest::Schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_supersedes_a_pending_poll() {
|
||||||
|
let mut gate = RefreshGate::default();
|
||||||
|
gate.poll();
|
||||||
|
assert!(gate.debouncing());
|
||||||
|
|
||||||
|
// The request starts its own short debounce instead of waiting out the poll.
|
||||||
|
assert_eq!(gate.request(), RefreshRequest::Schedule);
|
||||||
|
assert!(gate.debouncing());
|
||||||
|
assert!(
|
||||||
|
!gate.take_poll(),
|
||||||
|
"the superseded poll timer must be skipped"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The request's own debounce still fires.
|
||||||
|
gate.begin();
|
||||||
|
assert!(gate.running());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn poll_timer_runs_when_not_superseded() {
|
||||||
|
let mut gate = RefreshGate::default();
|
||||||
|
gate.poll();
|
||||||
|
assert!(gate.take_poll());
|
||||||
|
assert!(gate.idle());
|
||||||
|
gate.begin();
|
||||||
|
assert!(gate.running());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn poll_keeps_scheduling_until_a_request_preempts() {
|
||||||
|
let mut gate = RefreshGate::default();
|
||||||
|
gate.poll();
|
||||||
|
assert!(gate.take_poll());
|
||||||
|
gate.begin();
|
||||||
|
gate.finish();
|
||||||
|
gate.poll();
|
||||||
|
gate.request();
|
||||||
|
assert!(!gate.take_poll(), "preempted by the request");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_after_a_superseded_poll_is_folded_into_the_new_debounce() {
|
||||||
|
let mut gate = RefreshGate::default();
|
||||||
|
gate.poll();
|
||||||
|
gate.request();
|
||||||
|
assert_eq!(gate.request(), RefreshRequest::Fold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1133,6 +1133,7 @@ impl RepoStore {
|
|||||||
self.last_error = None;
|
self.last_error = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
|
let checkouts = CheckoutsStore::global(cx);
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
let push = backend.update(cx, |backend, cx| {
|
let push = backend.update(cx, |backend, cx| {
|
||||||
backend.push_checkout(announcement, path.clone(), head, cx)
|
backend.push_checkout(announcement, path.clone(), head, cx)
|
||||||
@@ -1147,10 +1148,8 @@ impl RepoStore {
|
|||||||
match &result {
|
match &result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
// The remote moved, so recompute the ready-to-push statuses.
|
// The remote moved, so recompute the ready-to-push statuses.
|
||||||
let checkouts = CheckoutsStore::global(cx);
|
|
||||||
checkouts.update(cx, |store, cx| {
|
checkouts.update(cx, |store, cx| {
|
||||||
store.request_push_statuses(&addr, cx);
|
store.checkout_pushed(&addr, &path, cx);
|
||||||
cx.notify();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user