From d4a7003a9e5ce21d1705326c51ee54d90f92c82b Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 15 Oct 2026 19:38:59 +0700 Subject: [PATCH] Add SyncStatusDirectory for per-surface status caching --- crates/signed_state/src/lib.rs | 2 +- crates/signed_state/src/sync_status.rs | 125 ++++++++++++++++++++++++- docs/SYNC_STATUS_DIRECTORIES.md | 1 - 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/crates/signed_state/src/lib.rs b/crates/signed_state/src/lib.rs index 2c52b8c..c757f22 100644 --- a/crates/signed_state/src/lib.rs +++ b/crates/signed_state/src/lib.rs @@ -29,7 +29,7 @@ pub use repos::RepoListStore; pub use signed_git::{Nip34Binding, Nip34Kind}; use signed_nostr::NostrBackend; pub use state::{GitPushMode, GitRef, PushStep, PushStepStatus, SyncState}; -pub use sync_status::{CheckoutSyncStatus, SyncEvent, SyncStatusStore}; +pub use sync_status::{CheckoutSyncStatus, SyncEvent, SyncStatusDirectory, SyncStatusStore}; pub fn init( db_path: impl AsRef, diff --git a/crates/signed_state/src/sync_status.rs b/crates/signed_state/src/sync_status.rs index a534f99..1ce345c 100644 --- a/crates/signed_state/src/sync_status.rs +++ b/crates/signed_state/src/sync_status.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::time::Duration; @@ -205,7 +205,15 @@ impl SyncStatusStore { }); self.tasks.push(cx.spawn(async move |this, cx| { - let updates = work.await?; + // Reset the slot even on failure, or every later pass folds forever. + let updates = match work.await { + Ok(updates) => updates, + Err(error) => { + log::warn!("failed to compute nostr sync statuses: {error}"); + Vec::new() + } + }; + let mut again = false; this.update(cx, |this, cx| { @@ -244,3 +252,116 @@ impl SyncStatusStore { })); } } + +/// Sync status cache bound to one panel or screen. +pub struct SyncStatusDirectory { + statuses: HashMap>, + pending: RefCell>, + _subscriptions: Vec, +} + +impl SyncStatusDirectory { + /// Create a directory, following the sync store's events. + pub fn new(cx: &mut App) -> Entity { + let store = SyncStatusStore::global(cx); + + cx.new(|cx| { + let subscription = + cx.subscribe(&store, |this: &mut Self, _store, event, cx| match event { + SyncEvent::Updated(updates) => this.apply(updates.clone(), cx), + }); + + Self { + statuses: HashMap::new(), + pending: RefCell::new(HashSet::new()), + _subscriptions: vec![subscription], + } + }) + } + + /// Queue a probe for addrs this surface renders. + pub fn track(&mut self, addrs: impl IntoIterator, cx: &mut Context) { + let missing: Vec = addrs + .into_iter() + .filter(|addr| { + !self.statuses.contains_key(addr) + && !self.with_pending(|pending| pending.contains(addr)) + }) + .collect(); + + if missing.is_empty() { + return; + } + + self.with_pending(|pending| pending.extend(missing.iter().cloned())); + + let store = SyncStatusStore::global(cx); + store.read(cx).probe(missing); + } + + /// Statuses for a repo, or empty; a miss queues a probe. + pub fn statuses_of(&self, addr: &RepoAddr, cx: &App) -> Vec { + self.queue(addr, cx); + self.statuses.get(addr).cloned().unwrap_or_default() + } + + /// Commits available locally but missing on Nostr. + pub fn unsynced(&self, addr: &RepoAddr, cx: &App) -> usize { + self.queue(addr, cx); + + self.statuses + .get(addr) + .map(|list| { + list.iter() + .map(|checkout| checkout.status.ahead_total) + .sum() + }) + .unwrap_or(0) + } + + fn queue(&self, addr: &RepoAddr, cx: &App) { + if self.with_pending(|pending| pending.insert(addr.clone())) { + let store = SyncStatusStore::global(cx); + store.read(cx).probe([addr.clone()]); + } + } + + fn apply(&mut self, updates: Vec<(RepoAddr, Vec)>, cx: &mut Context) { + if updates.is_empty() { + let changed = !self.statuses.is_empty(); + self.statuses.clear(); + self.with_pending(|pending| pending.clear()); + + if changed { + cx.notify(); + } + + return; + } + + let mut changed = false; + + for (addr, list) in updates { + let relevant = self.statuses.contains_key(&addr) + || self.with_pending(|pending| pending.remove(&addr)); + + if !relevant { + continue; + } + + if self.statuses.get(&addr) != Some(&list) { + self.statuses.insert(addr, list); + changed = true; + } + } + + if changed { + cx.notify(); + } + } + + fn with_pending(&self, then: impl FnOnce(&mut HashSet) -> R) -> R { + let mut pending = self.pending.borrow_mut(); + then(&mut pending) + } +} diff --git a/docs/SYNC_STATUS_DIRECTORIES.md b/docs/SYNC_STATUS_DIRECTORIES.md index 392ddef..24f436c 100644 --- a/docs/SYNC_STATUS_DIRECTORIES.md +++ b/docs/SYNC_STATUS_DIRECTORIES.md @@ -153,7 +153,6 @@ impl SyncStatusStore { pub struct SyncStatusDirectory { statuses: HashMap>, pending: RefCell>, - tasks: Vec>, _subscriptions: Vec, }