chore: refactor the backend #17

Merged
reya merged 10 commits from chore/refactor into master 2026-09-10 09:43:36 +00:00
7 changed files with 144 additions and 192 deletions
Showing only changes of commit 3cfdffdf82 - Show all commits
+56 -104
View File
@@ -1,11 +1,9 @@
use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::time::Duration;
use anyhow::{Context as AnyhowContext, Error, anyhow, bail};
use bitcoin_hashes::sha1::Hash as Sha1Hash;
@@ -35,9 +33,6 @@ pub const BOOTSTRAP_RELAYS: [&str; 4] = [
/// Relays used to index the user's NIP-65 relay list.
pub const INDEXER_RELAYS: [&str; 2] = ["wss://indexer.coracle.social", "wss://user.kindpag.es"];
/// How long an identical fetch or sync request is suppressed after it started.
const FETCH_DEDUP_WINDOW: Duration = Duration::from_secs(5 * 60);
#[derive(Debug, Clone)]
pub enum BackendEvent {
/// User has no signer configured.
@@ -82,11 +77,8 @@ pub struct Backend {
sync_progress: Option<(u64, u64)>,
/// True when the stored credential is NIP-49 encrypted.
passphrase_required: bool,
/// Fingerprints of recently started fetches and syncs, a relay plus filter set.
recent_fetches: HashMap<u64, Instant>,
/// Repositories with a push in flight, mirror or checkout based.
pushing_repos: Arc<Mutex<HashSet<RepoAddr>>>,
tasks: Vec<Task<Result<(), Error>>>,
}
struct GlobalBackend(Entity<Backend>);
@@ -124,7 +116,7 @@ impl Backend {
pub(crate) fn new(client: Client, signer: UniversalSigner, cx: &mut Context<Self>) -> Self {
let pump_client = client.clone();
let pump = cx.spawn(async move |this, cx| {
let pump: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let mut notifications = pump_client.notifications();
while let Some(notification) = notifications.next().await {
@@ -145,29 +137,21 @@ impl Backend {
Ok(())
});
pump.detach();
let mut this = Self {
client,
signer,
current_user: None,
sync_progress: None,
passphrase_required: false,
recent_fetches: HashMap::new(),
pushing_repos: Arc::new(Mutex::new(HashSet::new())),
tasks: vec![pump],
};
this.bootstrap(cx);
this
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
fn push_task(&mut self, task: Task<Result<(), Error>>) {
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
}
/// Bootstrap the client.
///
/// Restore the saved session, if any.
@@ -188,7 +172,7 @@ impl Backend {
Ok::<(), Error>(())
});
self.push_task(cx.spawn(async move |this, cx| {
let notify_task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
match task.await {
Ok(()) => {
this.update(cx, |_this, cx| cx.notify())?;
@@ -198,7 +182,8 @@ impl Backend {
}
}
Ok(())
}));
});
notify_task.detach();
self.restore_session(cx);
}
@@ -216,7 +201,7 @@ impl Backend {
let user = cx.read_credentials(USER_KEYRING);
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let content = match user.await {
Ok(Some((_username, secret))) => String::from_utf8(secret)?,
_ => {
@@ -264,7 +249,8 @@ impl Backend {
}
Ok(())
}));
});
task.detach();
}
/// Decrypt the NIP-49 keyring credential with the given passphrase.
@@ -982,14 +968,15 @@ impl Backend {
let pubkey = keys.public_key().to_hex();
let write = cx.write_credentials(USER_KEYRING, &pubkey, nsec.as_bytes());
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
if let Err(e) = write.await {
this.update(cx, |_, cx| cx.emit(BackendEvent::error(e.to_string())))?;
return Ok(());
}
this.update(cx, |this, cx| this.set_signer(keys, cx))?;
Ok(())
}));
});
task.detach();
}
/// Login with a `bunker://...` URI, NIP-46.
@@ -1008,7 +995,7 @@ impl Backend {
let credential = with_master_key(&uri_string, &keys);
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let result = async {
let mut signer = NostrConnect::new(
connect_uri,
@@ -1033,14 +1020,15 @@ impl Backend {
}
Ok(())
}));
});
task.detach();
}
/// Remove the saved credential and reset to an anonymous session.
pub fn logout(&mut self, cx: &mut Context<Self>) {
let delete = cx.delete_credentials(USER_KEYRING);
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
delete.await.ok();
this.update(cx, |this, cx| {
@@ -1053,14 +1041,15 @@ impl Backend {
})?;
Ok(())
}));
});
task.detach();
}
/// Fetch the user's grasp list and add the listed grasp servers as relays.
fn bootstrap_user(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
let client = self.client.clone();
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let result = async {
let events: Vec<Event> = client
.fetch_events(filters::grasp_list(public_key))
@@ -1082,7 +1071,8 @@ impl Backend {
}
Ok(())
}));
});
task.detach();
}
/// Get the nostr client.
@@ -1123,7 +1113,7 @@ impl Backend {
<T as AsyncSignEvent>::Error: std::error::Error + Send + Sync + 'static,
<T as AsyncNip44>::Error: std::error::Error + Send + Sync + 'static,
{
let task = cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
match new_signer.get_public_key_async().await {
Ok(public_key) => {
this.update(cx, |this, cx| {
@@ -1144,7 +1134,7 @@ impl Backend {
Ok(())
});
self.push_task(task);
task.detach();
}
/// Add relays and connect to them.
@@ -1159,7 +1149,7 @@ impl Backend {
Ok::<(), Error>(())
});
self.push_task(cx.spawn(async move |this, cx| {
let notify_task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
match task.await {
Ok(()) => {
this.update(cx, |_this, cx| cx.notify())?;
@@ -1169,74 +1159,49 @@ impl Backend {
}
}
Ok(())
}));
}
/// Whether an identical fetch started within [`FETCH_DEDUP_WINDOW`] is still recent.
///
/// Records the fingerprint when returning `false`, pruning expired entries first.
fn fetch_recently_started(&mut self, fingerprint: u64) -> bool {
self.recent_fetches
.retain(|_, started| started.elapsed() < FETCH_DEDUP_WINDOW);
if self.recent_fetches.contains_key(&fingerprint) {
return true;
}
self.recent_fetches.insert(fingerprint, Instant::now());
false
});
notify_task.detach();
}
/// Connect to a repository's announced relays, its NIP-34 `relays` tag.
///
/// Callers are responsible for not repeating this for relays they already
/// connected, e.g. `RepoStore::repo_relays`.
pub fn connect_repo_relays(
&mut self,
relays: Vec<RelayUrl>,
filters: Vec<Filter>,
cx: &mut Context<Self>,
) {
let relay_strs: Vec<&str> = relays.iter().map(|url| url.as_str()).collect();
let fingerprint = fetch_fingerprint(&relay_strs, &filters);
if self.fetch_recently_started(fingerprint) {
log::debug!("skipping duplicate repo relay fetch");
return;
}
let client = self.client.clone();
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |_this, _cx| {
if let Err(e) = connect_repo_relays(&client, relays, filters).await {
log::warn!("repo relay fetch failed: {e}");
// Allow an immediate retry after a failure.
this.update(cx, |this, _cx| {
this.recent_fetches.remove(&fingerprint);
})
.ok();
}
Ok(())
}));
});
task.detach();
}
/// One-shot subscription on the bootstrap relays only.
pub fn subscribe_bootstrap(&mut self, filters: Vec<Filter>, cx: &mut Context<Self>) {
let client = self.client.clone();
let task =
let fetch =
cx.background_spawn(async move { subscribe_bootstrap_only(&client, filters).await });
self.push_task(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
if let Err(e) = fetch.await {
this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?;
}
Ok(())
}));
});
task.detach();
}
/// Negentropy-sync the given filter against the bootstrap relays.
pub fn sync_bootstrap(&mut self, filter: Filter, cx: &mut Context<Self>) {
let fingerprint = fetch_fingerprint(&BOOTSTRAP_RELAYS, std::slice::from_ref(&filter));
if self.fetch_recently_started(fingerprint) {
log::debug!("skipping duplicate bootstrap sync");
return;
}
let client = self.client.clone();
self.sync_progress = Some((0, 0));
@@ -1244,7 +1209,7 @@ impl Backend {
let (tx, mut rx) = SyncProgress::channel();
self.push_task(cx.spawn(async move |this, cx| {
let progress_task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let mut last_percent: u64 = 0;
while rx.changed().await.is_ok() {
@@ -1270,15 +1235,16 @@ impl Backend {
}
Ok(())
}));
});
progress_task.detach();
let task = cx.background_spawn(async move {
let sync = cx.background_spawn(async move {
let opts = SyncOptions::default().progress(tx);
sync_bootstrap_only(&client, filter, opts).await
});
self.push_task(cx.spawn(async move |this, cx| {
match task.await {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
match sync.await {
Ok(summary) => {
log::debug!(
"sync done: {} received, {} sent",
@@ -1294,14 +1260,13 @@ impl Backend {
Err(e) => {
this.update(cx, |this, cx| {
this.sync_progress = None;
// Allow an immediate retry after a failure.
this.recent_fetches.remove(&fingerprint);
cx.emit(BackendEvent::error(e.to_string()))
})?;
}
}
Ok(())
}));
});
task.detach();
}
/// Sign, broadcast and locally store an event.
@@ -1361,17 +1326,18 @@ impl Backend {
/// Sign, broadcast and store an event without awaiting the result.
fn send_fire_and_forget(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
let task = self.send(builder, cx);
let publish = self.send(builder, cx);
self.push_task(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
if let Err(e) = publish.await {
this.update(cx, |_this, cx| {
cx.emit(BackendEvent::error(e.to_string()));
})
.ok();
}
Ok(())
}));
});
task.detach();
}
/// Publish NIP-09 deletions for `events`, best-effort.
@@ -1387,14 +1353,15 @@ impl Backend {
tags.push(Tag::parse(["k", &event.kind.to_string()]).expect("valid kind tag"));
}
let task = self.send(EventBuilder::new(Kind::EventDeletion, "").tags(tags), cx);
let publish = self.send(EventBuilder::new(Kind::EventDeletion, "").tags(tags), cx);
self.push_task(cx.spawn(async move |_this, _cx| {
if let Err(e) = task.await {
let task: Task<Result<(), Error>> = cx.spawn(async move |_this, _cx| {
if let Err(e) = publish.await {
log::warn!("failed to retract repository events: {e}");
}
Ok(())
}));
});
task.detach();
}
}
@@ -1417,21 +1384,6 @@ async fn broadcast_event(client: &Client, event: &Event) -> Result<Event, Error>
Ok(event.clone())
}
/// Fingerprint of a relay and filter set, for fetch dedup.
///
/// Relays and filters are sorted first, so the fingerprint is order-independent.
fn fetch_fingerprint(relays: &[&str], filters: &[Filter]) -> u64 {
let mut relays: Vec<&str> = relays.to_vec();
relays.sort_unstable();
let mut filters: Vec<&Filter> = filters.iter().collect();
filters.sort_unstable();
let mut hasher = DefaultHasher::new();
relays.hash(&mut hasher);
filters.hash(&mut hasher);
hasher.finish()
}
/// Add the given relays, connect and fetch the filters.
async fn connect_repo_relays(
client: &Client,
+13 -23
View File
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::Error;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use gpui::{App, AppContext, Context, Entity, Global, Subscription};
use nostr::prelude::*;
use settings::{CheckoutRecord, SettingsStore};
use signed_core::{Announcement, RepoAddr};
@@ -105,7 +105,6 @@ pub struct CheckoutsStore {
/// The local pass runs a full pass again once this is older than the
/// reconciliation cadence, so remote moves still land.
last_full_sync: Option<Instant>,
tasks: Vec<Task<Result<(), Error>>>,
_subscriptions: Vec<Subscription>,
}
@@ -165,7 +164,6 @@ impl CheckoutsStore {
refresh: RefreshGate::default(),
local_pending: false,
last_full_sync: None,
tasks: Vec::new(),
_subscriptions: subscriptions,
};
@@ -176,14 +174,6 @@ impl CheckoutsStore {
store
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
fn push_task(&mut self, task: Task<Result<(), Error>>) {
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
}
/// Remember a successful local-checkout use.
pub fn record(&mut self, path: PathBuf, addr: RepoAddr, cx: &mut Context<Self>) {
if cfg!(target_arch = "wasm32") {
@@ -302,12 +292,11 @@ impl CheckoutsStore {
return;
}
let task = cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
this.update(cx, |this, cx| this.run_refresh(cx))
});
self.push_task(task);
})
.detach();
}
/// One full resolve and apply cycle, the debounced entry point.
@@ -388,7 +377,7 @@ impl CheckoutsStore {
Ok::<_, Error>((associations, statuses, push_statuses))
});
self.push_task(cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
let (associations, statuses, push_statuses) = match work.await {
Ok(results) => results,
Err(_) => {
@@ -435,7 +424,8 @@ impl CheckoutsStore {
})?;
Ok(())
}));
})
.detach();
}
/// Schedule the fast local status pass, unless one is already pending.
@@ -449,15 +439,14 @@ impl CheckoutsStore {
}
self.local_pending = true;
let task = cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
cx.background_executor().timer(LOCAL_POLL).await;
this.update(cx, |this, cx| {
this.local_pending = false;
this.local_tick(cx);
})
});
self.push_task(task);
})
.detach();
}
/// The fast local status pass.
@@ -525,7 +514,7 @@ impl CheckoutsStore {
Ok::<_, Error>((statuses, push_statuses))
});
self.push_task(cx.spawn(async move |this, cx| {
let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let Ok((statuses, push_statuses)) = work.await else {
// Git reads are best-effort, keep the last results.
return Ok(());
@@ -550,7 +539,8 @@ impl CheckoutsStore {
})?;
Ok(())
}));
});
task.detach();
}
}
+4 -5
View File
@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Error;
use gpui::{App, AppContext, Context, Entity, Global, Task};
use gpui::{App, AppContext, Context, Entity, Global};
use signed_git::find_git_repos;
struct GlobalLocalReposStore(Entity<LocalReposStore>);
@@ -19,7 +19,6 @@ pub struct LocalReposStore {
pub scanning: bool,
/// A scan was requested while one was already running.
scan_dirty: bool,
tasks: Vec<Task<Result<(), Error>>>,
}
impl LocalReposStore {
@@ -39,7 +38,6 @@ impl LocalReposStore {
repos: Arc::new(Vec::new()),
scanning: false,
scan_dirty: false,
tasks: Vec::new(),
};
store.rescan(cx);
store
@@ -81,7 +79,7 @@ impl LocalReposStore {
repos
});
self.tasks.push(cx.spawn(async move |this, cx| {
let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let repos = work.await;
let again = this.update(cx, |this, cx| {
this.repos = Arc::new(repos);
@@ -99,6 +97,7 @@ impl LocalReposStore {
}
Ok(())
}));
});
task.detach();
}
}
+12 -20
View File
@@ -75,7 +75,6 @@ pub struct ProfileStore {
seen: RefCell<HashSet<PublicKey>>,
/// Sender for queuing fetch requests, batched by a background task.
sender: Sender<PublicKey>,
tasks: Vec<Task<Result<(), Error>>>,
_subscription: Subscription,
}
@@ -114,17 +113,15 @@ impl ProfileStore {
let (sender, receiver) = flume::unbounded::<PublicKey>();
let entity = cx.entity().downgrade();
let mut tasks = Vec::new();
tasks.push(cx.spawn(async move |_this, cx| {
cx.spawn(async move |_this, cx| {
Self::handle_requests(entity, &client, &receiver, cx).await
}));
})
.detach();
let mut store = Self {
profiles: HashMap::new(),
seen: RefCell::new(HashSet::new()),
sender,
tasks,
_subscription: subscription,
};
@@ -132,14 +129,6 @@ impl ProfileStore {
store
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
fn push_task(&mut self, task: Task<Result<(), Error>>) {
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
}
/// Get a profile.
///
/// Returns a placeholder with default metadata. Queues a fetch when the profile is not cached yet.
@@ -181,7 +170,7 @@ impl ProfileStore {
Ok::<_, Error>(profiles)
});
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let profiles = work.await?;
this.update(cx, |this, cx| {
@@ -192,7 +181,8 @@ impl ProfileStore {
})?;
Ok(())
}));
});
task.detach();
}
/// Re-read the latest metadata of an author from the local database.
@@ -217,7 +207,7 @@ impl ProfileStore {
Ok::<_, Error>(profile)
});
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let profile = work.await?;
this.update(cx, |this, cx| {
@@ -228,7 +218,8 @@ impl ProfileStore {
})?;
Ok(())
}));
});
task.detach();
}
/// Re-read the latest metadata of every requested author from the local database.
@@ -273,7 +264,7 @@ impl ProfileStore {
Ok::<_, Error>(profiles)
});
self.push_task(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let profiles = work.await?;
this.update(cx, |this, cx| {
@@ -284,7 +275,8 @@ impl ProfileStore {
})?;
Ok(())
}));
});
task.detach();
}
/// Sync metadata for requested authors in batches, debounced to collect requests.
+20 -21
View File
@@ -81,7 +81,6 @@ pub struct RepoStore {
root_fetches: HashSet<EventId>,
/// Refresh coalescing, see [`RefreshGate`].
refresh: RefreshGate,
tasks: Vec<Task<Result<(), Error>>>,
_subscription: Subscription,
}
@@ -148,7 +147,6 @@ impl RepoStore {
root_fetches: HashSet::new(),
refresh: RefreshGate::default(),
_subscription: subscription,
tasks: Vec::new(),
};
store.subscribe_remote(cx);
@@ -229,14 +227,12 @@ impl RepoStore {
return;
}
let task = cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
this.update(cx, |this, cx| this.run_refresh(cx))
});
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
})
.detach();
}
fn run_refresh(&mut self, cx: &mut Context<Self>) {
@@ -372,9 +368,7 @@ impl RepoStore {
))
});
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
let (
announcement,
state,
@@ -466,7 +460,8 @@ impl RepoStore {
}
Ok(())
}));
})
.detach();
}
/// Resolve the status of a root event, an issue, patch or PR, per NIP-34.
@@ -638,7 +633,7 @@ impl RepoStore {
.collect()
};
self.tasks.push(cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
// The PR references the root patch event.
// Viewers can then find the patch without carrying it inline.
let root_patch = match publish_patch_series(
@@ -822,7 +817,8 @@ impl RepoStore {
}
Ok(())
}));
})
.detach();
}
/// Update a pull request.
@@ -894,7 +890,7 @@ impl RepoStore {
.map(|a| a.clone.clone())
.unwrap_or_default();
self.tasks.push(cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
if let Err(e) = publish_patch_series(
&this,
cx,
@@ -943,7 +939,8 @@ impl RepoStore {
}
Ok(())
}));
})
.detach();
}
/// Set the status of a root event.
@@ -1040,7 +1037,7 @@ impl RepoStore {
Ok::<_, Error>(applied)
});
self.tasks.push(cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
match apply.await {
Ok(applied) => {
this.update(cx, |this, cx| {
@@ -1062,7 +1059,8 @@ impl RepoStore {
}
}
Ok(())
}));
});
task.detach();
}
/// The latest announcement of this repository,
@@ -1335,17 +1333,18 @@ impl RepoStore {
self.last_error = None;
let backend = Backend::global(cx);
let task = backend.update(cx, |backend, cx| backend.send(builder, cx));
let publish = backend.update(cx, |backend, cx| backend.send(builder, cx));
self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
if let Err(e) = publish.await {
this.update(cx, |this, cx| {
this.last_error = Some(e.to_string());
cx.notify();
})?;
}
Ok(())
}));
});
task.detach();
}
}
+7 -17
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::time::Duration;
use anyhow::Error;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use gpui::{App, AppContext, Context, Entity, Global, Subscription};
use nostr_sdk::prelude::*;
use signed_core::{Announcement, Deletions, RepoAddr, filters, repo_addr};
@@ -55,7 +55,6 @@ pub struct RepoListStore {
pub counts: Arc<HashMap<RepoAddr, RepoActivityCounts>>,
/// Refresh coalescing, see [`RefreshGate`].
refresh: RefreshGate,
tasks: Vec<Task<Result<(), Error>>>,
_subscription: Subscription,
}
@@ -114,7 +113,6 @@ impl RepoListStore {
counts: Arc::new(HashMap::new()),
refresh: RefreshGate::default(),
_subscription: subscription,
tasks: Vec::new(),
};
store.subscribe_remote(cx);
@@ -133,14 +131,6 @@ impl RepoListStore {
.collect()
}
/// Track a spawned task, pruning finished tasks first.
///
/// Keeps the store's task list bounded by the number of in-flight tasks.
fn push_task(&mut self, task: Task<Result<(), Error>>) {
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(task);
}
/// Negentropy-sync announcements with the bootstrap relays.
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
let backend = Backend::global(cx);
@@ -171,13 +161,12 @@ impl RepoListStore {
return;
}
let task = cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
this.update(cx, |this, cx| this.run_refresh(cx))
});
self.push_task(task);
})
.detach();
}
/// One query and apply cycle, the debounced entry point.
@@ -294,7 +283,7 @@ impl RepoListStore {
Ok::<_, Error>((announcements, last_activity, counts))
});
self.push_task(cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| {
let (announcements, last_activity, counts) = match work.await {
Ok(results) => results,
// Database errors are transient, keep the last list.
@@ -321,6 +310,7 @@ impl RepoListStore {
}
Ok(())
}));
})
.detach();
}
}
+32 -2
View File
@@ -440,6 +440,18 @@ octal-escaped/non-ASCII quoted paths) before committing to the swap.
## 6. Remove the `tasks: Vec<Task<...>>` + `push_task` boilerplate — use `Task::detach()`
> **Status: done.** Removed the `tasks` field and `push_task` from all six
> stores (`backend.rs`, `checkouts.rs`, `local_repos.rs`, `profile.rs`,
> `repo.rs`, `repo_list.rs`); every call site now ends in `.detach()`
> instead. As with §14, most `cx.spawn` sites lost their type-inference
> anchor and needed an explicit `let task: Task<Result<(), Error>> = ...`
> (or `gpui::Task<...>` where `Task` wasn't imported) before `.detach()`.
> A few closures that captured a variable also named `task` (the awaited
> inner task) were given a distinct outer name (`notify_task`, `publish`,
> `fetch`, `sync`) to avoid a confusing shadow. `cargo check --workspace`
> and `cargo test -p signed_state` (24 tests) / `cargo test -p workspace`
> (14 tests) all pass.
Verified against the actual pinned GPUI revision
(`~/.cargo/git/checkouts/zed-a70e2ad075855582/1870e26/crates/scheduler/src/executor.rs:375-573`
and `crates/gpui/src/executor.rs:32-63`).
@@ -952,6 +964,14 @@ entirely disjoint observers.
## 14. `crates/workspace` has the same task-list pattern as §6 — and there it's an actual bug
> **Status: done.** The `tasks` field and all 17 push sites were removed from
> `RepoDetailView`, `NewPullRequestView`, `CommitDiffView` and
> `PullRequestDetailView`, replaced with `.detach()`. `cargo check -p workspace`
> and `cargo test -p workspace` (14 tests) pass. Removing the field cost each
> `cx.spawn`/`cx.spawn_in` call site its type-inference anchor, so every
> remaining spawn site needed an explicit `let task: gpui::Task<Result<(), ...>> = ...`
> annotation — expect the same when doing §6's `signed_state` half.
§6 covers `signed_state`'s 6 stores, where the unpruned-`Vec<Task>` pattern
is a style/complexity concern with no observed failure, because
`push_task` always pruned before pushing. `crates/workspace` has the exact
@@ -1219,14 +1239,24 @@ method.
## Action plan, in order of risk/reward
1. **Delete the fetch/sync dedup cache** (§3). Pure removal, no behavior
1. **Delete the fetch/sync dedup cache** (§3). Pure removal, no behavior
change for the intended usage pattern (each call site already has, or
trivially gets, its own guard). Lowest risk, do first.
2. **Remove the `tasks: Vec<Task<...>>` + `push_task` boilerplate**, in
Done: removed `recent_fetches`/`fetch_recently_started`/`fetch_fingerprint`/
`FETCH_DEDUP_WINDOW` and the now-unused `DefaultHasher`/`Hash`/`Hasher`/
`Instant` imports from `signed_state/src/backend.rs`. `connect_repo_relays`
and `sync_bootstrap` no longer fingerprint or gate on a cache; callers keep
their own guards (`RepoStore::repo_relays`, one-shot construction-time call
in `RepoListStore::subscribe_remote`). `cargo check --workspace` and
`cargo test -p signed_state` (24 tests) both pass unchanged.
2. ✅ **Remove the `tasks: Vec<Task<...>>` + `push_task` boilerplate**, in
both `signed_state` (§6) and `crates/workspace` (§14), in favor of
`.detach()`/`.detach_and_log_err(cx)`. Independent of every other change
here, touches 10 files, all mechanical — and fixes a real unbounded-growth
bug in `RepoDetailView`/`NewPullRequestView` along the way.
Done: both halves are complete, see §6 and §14 for details.
3. **Fix the relay add/connect calls** (§8): drop `.as_str()`/`ToString`
round trips, replace `add_relay` + blanket `client.connect()`/
`connect_relay` pairs with `add_relay(url).and_connect()`, and delete