diff --git a/crates/signed_state/src/backend.rs b/crates/signed_state/src/backend.rs index 59e3dd6..b59d900 100644 --- a/crates/signed_state/src/backend.rs +++ b/crates/signed_state/src/backend.rs @@ -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, /// Repositories with a push in flight, mirror or checkout based. pushing_repos: Arc>>, - tasks: Vec>>, } struct GlobalBackend(Entity); @@ -124,7 +116,7 @@ impl Backend { pub(crate) fn new(client: Client, signer: UniversalSigner, cx: &mut Context) -> Self { let pump_client = client.clone(); - let pump = cx.spawn(async move |this, cx| { + let pump: Task> = 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>) { - 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> = 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> = 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> = 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> = 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) { let delete = cx.delete_credentials(USER_KEYRING); - self.push_task(cx.spawn(async move |this, cx| { + let task: Task> = 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) { let client = self.client.clone(); - self.push_task(cx.spawn(async move |this, cx| { + let task: Task> = cx.spawn(async move |this, cx| { let result = async { let events: Vec = 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 { ::Error: std::error::Error + Send + Sync + 'static, ::Error: std::error::Error + Send + Sync + 'static, { - let task = cx.spawn(async move |this, cx| { + let task: Task> = 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> = 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, filters: Vec, cx: &mut Context, ) { - 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> = 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, cx: &mut Context) { 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> = 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) { - 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> = 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> = 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) { - 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> = 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> = 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 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, diff --git a/crates/signed_state/src/checkouts.rs b/crates/signed_state/src/checkouts.rs index 21758ab..f38f33d 100644 --- a/crates/signed_state/src/checkouts.rs +++ b/crates/signed_state/src/checkouts.rs @@ -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, - tasks: Vec>>, _subscriptions: Vec, } @@ -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>) { - 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) { 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> = 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(); } } diff --git a/crates/signed_state/src/local_repos.rs b/crates/signed_state/src/local_repos.rs index 776943d..19ace8f 100644 --- a/crates/signed_state/src/local_repos.rs +++ b/crates/signed_state/src/local_repos.rs @@ -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); @@ -19,7 +19,6 @@ pub struct LocalReposStore { pub scanning: bool, /// A scan was requested while one was already running. scan_dirty: bool, - tasks: Vec>>, } 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> = 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(); } } diff --git a/crates/signed_state/src/profile.rs b/crates/signed_state/src/profile.rs index c2d74ab..f1cf203 100644 --- a/crates/signed_state/src/profile.rs +++ b/crates/signed_state/src/profile.rs @@ -75,7 +75,6 @@ pub struct ProfileStore { seen: RefCell>, /// Sender for queuing fetch requests, batched by a background task. sender: Sender, - tasks: Vec>>, _subscription: Subscription, } @@ -114,17 +113,15 @@ impl ProfileStore { let (sender, receiver) = flume::unbounded::(); 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>) { - 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> = 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> = 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> = 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. diff --git a/crates/signed_state/src/repo.rs b/crates/signed_state/src/repo.rs index e2d2e1d..d516228 100644 --- a/crates/signed_state/src/repo.rs +++ b/crates/signed_state/src/repo.rs @@ -81,7 +81,6 @@ pub struct RepoStore { root_fetches: HashSet, /// Refresh coalescing, see [`RefreshGate`]. refresh: RefreshGate, - tasks: Vec>>, _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) { @@ -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> = 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> = 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(); } } diff --git a/crates/signed_state/src/repo_list.rs b/crates/signed_state/src/repo_list.rs index 58a4efd..92ca7cb 100644 --- a/crates/signed_state/src/repo_list.rs +++ b/crates/signed_state/src/repo_list.rs @@ -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>, /// Refresh coalescing, see [`RefreshGate`]. refresh: RefreshGate, - tasks: Vec>>, _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>) { - 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) { 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(); } } diff --git a/docs/backend-rearchitecture.md b/docs/backend-rearchitecture.md index 52a4a3c..fb50a31 100644 --- a/docs/backend-rearchitecture.md +++ b/docs/backend-rearchitecture.md @@ -440,6 +440,18 @@ octal-escaped/non-ASCII quoted paths) before committing to the swap. ## 6. Remove the `tasks: Vec>` + `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> = ...` +> (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> = ...` +> annotation — expect the same when doing §6's `signed_state` half. + §6 covers `signed_state`'s 6 stores, where the unpruned-`Vec` 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>` + `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>` + `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