This commit is contained in:
2026-09-05 15:27:35 +07:00
parent 1f3afea81b
commit 7ff09502b9
8 changed files with 314 additions and 182 deletions
+34 -8
View File
@@ -64,10 +64,17 @@ struct Remembered {
}
/// Global store of local-checkout associations and per-checkout statuses.
///
/// Readers (the sidebar rows, the repository panels) observe this store and
/// derive what they display from their own snapshots, so publishing needs no
/// fine-grained entities: the store notifies when a slice changed and each
/// reader re-derives only what it shows.
pub struct CheckoutsStore {
/// Checkout paths per announced repository.
by_repo: HashMap<RepoAddr, Vec<PathBuf>>,
/// Ready-to-contribute statuses of the requested repositories.
///
/// Those are the repository detail panels currently open.
statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>,
/// Repositories whose statuses are recomputed on every input change.
///
@@ -77,7 +84,7 @@ pub struct CheckoutsStore {
///
/// The sidebar rows of the user's own repositories and their detail panels.
push_requested: HashSet<RepoAddr>,
/// Ready-to-push statuses of the requested own repositories.
/// The ready-to-push statuses of the requested own repositories.
push_statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>,
/// Last announced head branch per requested repository.
///
@@ -85,8 +92,8 @@ pub struct CheckoutsStore {
requested_head: HashMap<RepoAddr, Option<String>>,
/// Refresh coalescing, see [`RefreshGate`].
refresh: RefreshGate,
_subscriptions: Vec<Subscription>,
tasks: Vec<Task<Result<(), Error>>>,
_subscriptions: Vec<Subscription>,
}
impl CheckoutsStore {
@@ -127,8 +134,9 @@ impl CheckoutsStore {
this.status_requested.clear();
this.push_requested.clear();
this.requested_head.clear();
this.statuses = HashMap::new();
this.push_statuses = HashMap::new();
this.statuses.clear();
this.push_statuses.clear();
cx.notify();
this.refresh(cx);
}
}));
@@ -142,8 +150,8 @@ impl CheckoutsStore {
push_statuses: HashMap::new(),
requested_head: HashMap::new(),
refresh: RefreshGate::default(),
_subscriptions: subscriptions,
tasks: Vec::new(),
_subscriptions: subscriptions,
};
if !cfg!(target_arch = "wasm32") {
@@ -217,7 +225,7 @@ impl CheckoutsStore {
/// The ready-to-contribute statuses of `addr`.
///
/// Empty while none are known or nothing is ahead.
pub fn statuses_of(&self, addr: &RepoAddr) -> Vec<CheckoutStatus> {
pub fn ready_statuses_of(&self, addr: &RepoAddr) -> Vec<CheckoutStatus> {
self.statuses.get(addr).cloned().unwrap_or_default()
}
@@ -228,13 +236,20 @@ impl CheckoutsStore {
}
/// The ready-to-push statuses of `addr`.
/// Only meaningful for repositories announced by the signed-in user.
///
/// Empty while none are known or nothing is unpushed.
pub fn push_statuses_of(&self, addr: &RepoAddr) -> Vec<CheckoutStatus> {
self.push_statuses.get(addr).cloned().unwrap_or_default()
}
/// The number of unpushed commits for a repository.
pub fn unpushed(&self, addr: &RepoAddr) -> usize {
self.push_statuses
.get(addr)
.map(|list| list.iter().map(|status| status.ahead as usize).sum())
.unwrap_or(0)
}
/// Re-resolve the associations and the requested statuses.
///
/// Requests arriving while a pass runs fold into a follow-up.
@@ -362,10 +377,20 @@ impl CheckoutsStore {
};
let again = this.update(cx, |this, cx| {
let associations_changed = this.by_repo != associations;
let statuses_changed = this.statuses != statuses;
let push_statuses_changed = this.push_statuses != push_statuses;
this.by_repo = associations;
this.statuses = statuses;
this.push_statuses = push_statuses;
cx.notify();
// Poll cycles and identity re-requests recompute the same maps
// over and over. Notify only when something actually changed,
// so observers skip the no-op heartbeats.
if associations_changed || statuses_changed || push_statuses_changed {
cx.notify();
}
this.refresh.finish()
})?;
@@ -378,6 +403,7 @@ impl CheckoutsStore {
this.update(cx, |this, cx| {
if poll && this.refresh.idle() {
this.refresh.debounce();
// Open panels get the fast cadence.
// Each cycle fetches every watched checkout's remote.
let delay = if this.status_requested.is_empty() {
+2 -2
View File
@@ -42,7 +42,7 @@ pub fn init(
let entity = cx.new(|cx| Backend::new(client, signer, cx));
Backend::set_global(entity.clone(), cx);
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
RepoListStore::set_global(cx.new(RepoListStore::new), cx);
GitStore::set_global(repos_root, cx);
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(scan_paths, cx)), cx);
CheckoutsStore::set_global(cx.new(CheckoutsStore::new), cx);
@@ -57,7 +57,7 @@ pub fn init(cx: &mut App) -> Entity<Backend> {
let entity = cx.new(|cx| Backend::new(client, signer, cx));
Backend::set_global(entity.clone(), cx);
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
RepoListStore::set_global(cx.new(RepoListStore::new), cx);
GitStore::set_global(PathBuf::new(), cx);
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(Vec::new(), cx)), cx);
CheckoutsStore::set_global(cx.new(|cx| CheckoutsStore::new(cx)), cx);
+18 -23
View File
@@ -42,7 +42,7 @@ impl RepoActivityCounts {
}
}
/// Store listing repository announcements, global discovery or per-author.
/// Store listing the discovered repository announcements, newest first.
pub struct RepoListStore {
/// Shared so views can clone the list per frame without a deep copy.
pub announcements: Arc<Vec<Announcement>>,
@@ -53,7 +53,6 @@ pub struct RepoListStore {
///
/// Used for the Popular ranking of the explore list.
pub counts: Arc<HashMap<RepoAddr, RepoActivityCounts>>,
author: Option<PublicKey>,
/// Refresh coalescing, see [`RefreshGate`].
refresh: RefreshGate,
tasks: Vec<Task<Result<(), Error>>>,
@@ -61,7 +60,7 @@ pub struct RepoListStore {
}
impl RepoListStore {
/// Retrieve the global explore store.
/// Retrieve the global repository list store.
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalRepoListStore>().0.clone()
}
@@ -70,8 +69,8 @@ impl RepoListStore {
cx.set_global(GlobalRepoListStore(entity));
}
/// Create a store. If `author` is `None`, all announcements are listed.
pub fn new(author: Option<PublicKey>, cx: &mut Context<Self>) -> Self {
/// Create the store listing all announcements.
pub fn new(cx: &mut Context<Self>) -> Self {
let backend = Backend::global(cx);
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
@@ -87,14 +86,11 @@ impl RepoListStore {
} else {
let is_announcement = update.kind == Kind::GitRepoAnnouncement;
let is_repo_state = update.kind == Kind::RepoState;
let tracked = is_announcement || is_repo_state;
tracked && this.author.is_none_or(|a| a == update.author)
is_announcement || is_repo_state
}
}
BackendEvent::Published(event) => {
let kind_match = event.kind == Kind::GitRepoAnnouncement;
let author_match = this.author.is_none_or(|a| a == event.pubkey);
let announcement = kind_match && author_match;
let announcement = event.kind == Kind::GitRepoAnnouncement;
// Locally published deletions are already in the local database.
// Refresh so they take effect immediately, like relay deletions.
@@ -116,7 +112,6 @@ impl RepoListStore {
announcements: Arc::new(Vec::new()),
last_activity: Arc::new(HashMap::new()),
counts: Arc::new(HashMap::new()),
author,
refresh: RefreshGate::default(),
_subscription: subscription,
tasks: Vec::new(),
@@ -129,6 +124,15 @@ impl RepoListStore {
store
}
/// The announcements of `user`, newest first.
pub fn announcements_of(&self, user: &PublicKey) -> Vec<Announcement> {
self.announcements
.iter()
.filter(|a| a.owner == *user)
.cloned()
.collect()
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
@@ -140,14 +144,9 @@ impl RepoListStore {
/// Negentropy-sync announcements with the bootstrap relays.
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
let backend = Backend::global(cx);
let author = self.author;
backend.update(cx, |backend, cx| {
let filter = match author {
Some(a) => filters::announcements_by(a),
None => filters::all_announcements(),
};
backend.sync_bootstrap(filter, cx);
backend.sync_bootstrap(filters::all_announcements(), cx);
// Deletion requests, NIP-09/62, must be known before any announcement is shown.
backend.sync_bootstrap(filters::deletions(), cx);
});
@@ -187,15 +186,11 @@ impl RepoListStore {
let backend = Backend::global(cx);
let client = backend.read(cx).client();
let author = self.author;
let work = cx.background_spawn(async move {
let filter = match author {
Some(a) => filters::announcements_by(a),
None => filters::all_announcements(),
};
let filter = filters::all_announcements();
let events = client.database().query(filter).await?;
let deletion_events = client.database().query(filters::deletions()).await?;
let deletions = Deletions::from_events(deletion_events);