3 Commits
Author SHA1 Message Date
reya 70006f74f0 optimize
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s
Rust / build (macos-latest, stable) (pull_request) Canceled after 0s
Rust / build (ubuntu-latest, stable) (pull_request) Canceled after 0s
Rust / build (windows-latest, stable) (pull_request) Canceled after 0s
2026-09-27 09:38:55 +07:00
reya 8ea320cd6f fix leak tasks 2026-09-27 09:24:41 +07:00
reya 0ee67d93b6 fix task leak in repo store 2026-09-27 09:14:32 +07:00
11 changed files with 104 additions and 146 deletions
+2
View File
@@ -81,6 +81,8 @@ Both `cx.spawn` and `cx.background_spawn` return a `Task<R>`, which is a future
A task which doesn't do anything but provide a value can be created with `Task::ready(value)`.
Prefer keeping a task in a field over `.detach()` when the work belongs to a view or store. A detached task outlives the view that started it (for example, a repository panel the user has closed), while a task stored in a field is cancelled when that struct drops. A finished `Task` held in a container is not reaped by GPUI - it stays alive until its handle is dropped - so if the container can accumulate many runs, either drop the finished handles before pushing a new one, or hold a single `Option<Task<_>>` and replace it.
## Elements
The `Render` trait is used to render some state into an element tree that is laid out using flexbox layout. An `Entity<T>` where `T` implements `Render` is sometimes called a "view".
+2 -2
View File
@@ -1066,7 +1066,7 @@ impl Backend {
)
.await?;
for url in user_grasp_list_servers(client.clone(), public_key).await? {
for url in user_grasp_list_servers(&client, public_key).await? {
client.add_relay(url).and_connect().await.ok();
}
@@ -1525,7 +1525,7 @@ fn latest_grasp_list_servers(events: Vec<Event>) -> Vec<RelayUrl> {
}
pub async fn user_grasp_list_servers(
client: Client,
client: &Client,
user: PublicKey,
) -> Result<Vec<RelayUrl>, Error> {
let events: Vec<Event> = client
+4 -46
View File
@@ -17,15 +17,9 @@ use crate::repos::RepoListStore;
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
/// How often the statuses are recomputed against the local refs.
///
/// A commit lands in a checkout long before the remote reconciliation cadence,
/// so this fast pass surfaces ready-to-push and ready-to-contribute checkouts
/// within a second or two. It reads the tracking refs only, no network.
const LOCAL_POLL: Duration = Duration::from_secs(2);
/// How often a full pass refreshes the remotes while any repository panel is open.
const STATUS_POLL: Duration = Duration::from_secs(15);
/// Remote refresh interval for the `ready to push` badges of the user's own repositories.
const PUSH_POLL: Duration = Duration::from_secs(60);
@@ -46,10 +40,6 @@ pub struct CheckoutStatus {
/// Commit the branch points at, for tip-based PR dedupe.
pub head: String,
/// What the branch is compared against.
/// For ready-to-contribute statuses, the announced HEAD branch.
/// The fallbacks are `main`, then the first local branch.
/// For ready-to-push statuses, the remote-tracking ref.
/// Unpushed commits are counted against it.
///
/// It is `refs/remotes/origin/<branch>`, else `origin/HEAD` for new branches.
pub base: String,
@@ -67,11 +57,6 @@ 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>>,
@@ -144,6 +129,7 @@ impl CheckoutsStore {
this.statuses.clear();
this.push_statuses.clear();
cx.notify();
this.refresh(cx);
}
}));
@@ -282,9 +268,6 @@ impl CheckoutsStore {
}
/// Re-resolve the associations and the requested statuses.
///
/// Requests arriving while a pass runs fold into a follow-up, requests
/// arriving while the debounce timer is pending are dropped.
pub fn refresh(&mut self, cx: &mut Context<Self>) {
if self.debounce_pending || self.refresh.request() != RefreshRequest::Schedule {
return;
@@ -300,12 +283,6 @@ impl CheckoutsStore {
}
/// One full resolve and apply cycle, the debounced entry point.
///
/// Re-resolves the associations from the settings, the scan and the
/// announcements, then recomputes the requested statuses against freshly
/// fetched remotes. Full passes run on every input change and on the
/// remote reconciliation cadence ([`Self::local_tick`]); they also restart
/// the fast local pass.
fn run_refresh(&mut self, cx: &mut Context<Self>) {
self.debounce_pending = false;
self.refresh.begin();
@@ -431,10 +408,6 @@ impl CheckoutsStore {
}
/// Schedule the fast local status pass, unless one is already pending.
///
/// Every [`LOCAL_POLL`] the pass recomputes the requested statuses against
/// the local refs, with no network, so a new commit in a checkout surfaces in
/// a second or two instead of at the next remote reconciliation.
fn schedule_local_pass(&mut self, cx: &mut Context<Self>) {
if self.local_pending {
return;
@@ -452,10 +425,6 @@ impl CheckoutsStore {
}
/// The fast local status pass.
///
/// Recomputes the statuses against the local refs; when the remote
/// reconciliation cadence elapsed, it runs a full pass instead so pushes
/// made elsewhere do not linger as `to push`.
fn local_tick(&mut self, cx: &mut Context<Self>) {
// Nothing watched: the chain idles out until a new request restarts it.
if self.status_requested.is_empty() && self.push_requested.is_empty() {
@@ -490,10 +459,6 @@ impl CheckoutsStore {
}
/// Recompute the requested statuses against the tracking refs only.
///
/// The refs were last refreshed by a full pass. Comparing against them is
/// enough to pick up new local commits, and skipping the network keeps
/// this pass cheap enough to run every [`LOCAL_POLL`].
fn run_local_statuses(&mut self, cx: &mut Context<Self>) {
let associations = self.by_repo.clone();
@@ -635,10 +600,6 @@ fn checkout_status(path: &Path, announced_head: Option<&str>) -> Option<Checkout
}
/// The `ready to push` status of one checkout of the user's own repository.
///
/// `fetch` refreshes the remote heads first, so a full pass sees pushes made
/// elsewhere; the fast local pass skips it and compares against the tracking
/// refs left by the last full pass, which is enough to detect local commits.
fn checkout_push_status(path: &Path, fetch: bool) -> Option<CheckoutStatus> {
if signed_git::worktree_dirty(path) {
return None;
@@ -649,8 +610,6 @@ fn checkout_push_status(path: &Path, fetch: bool) -> Option<CheckoutStatus> {
let origin = signed_git::origin_url(path).ok().flatten()?;
if fetch {
// Refresh the remote heads first.
// Commits made elsewhere or pushed from another machine must not linger as `to push`.
signed_git::fetch_repo_refs(path, &[origin], "+refs/heads/*:refs/remotes/origin/*").ok();
}
@@ -678,10 +637,6 @@ fn checkout_push_status(path: &Path, fetch: bool) -> Option<CheckoutStatus> {
}
/// Compute the requested statuses against the checkout paths of `associations`.
///
/// Shared by the full and the local pass. `fetch` refreshes the checkouts'
/// remote heads first, so the full pass sees remote moves; the fast local
/// pass reads the tracking refs only, which is enough to detect local commits.
fn compute_statuses(
associations: &HashMap<RepoAddr, Vec<PathBuf>>,
requested: &[(RepoAddr, Option<String>)],
@@ -737,12 +692,14 @@ pub fn pr_proposes_checkout(
if pr.kind != Kind::GitPullRequest || !open || pr.pubkey != user {
return false;
}
let branch_matches = pr
.tags
.iter()
.find(|t| t.kind() == "branch-name")
.and_then(|t| t.content())
.is_some_and(|name| name == checkout.branch);
// A renamed branch falls back to the proposed tip commit.
let tip_matches = pr
.tags
@@ -750,6 +707,7 @@ pub fn pr_proposes_checkout(
.find(|t| t.kind() == "c")
.and_then(|t| t.content())
.is_some_and(|tip| tip == checkout.head);
branch_matches || tip_matches
}
+7 -2
View File
@@ -104,11 +104,16 @@ impl Inbox {
/// Sign the state with a random key and store it locally.
fn persist(&mut self, cx: &mut Context<Self>) {
let Some(me) = Backend::global(cx).read(cx).current_user() else {
let backend = Backend::global(cx);
let (me, client) = {
let backend = backend.read(cx);
(backend.current_user(), backend.client())
};
let Some(me) = me else {
return;
};
let client = Backend::global(cx).read(cx).client();
let state = self.state.clone();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
-4
View File
@@ -18,10 +18,6 @@ impl RefreshGate {
self.running
}
/// A new refresh request arrived.
///
/// Folded into a follow-up run while one is in flight, otherwise the
/// caller starts the run itself.
pub fn request(&mut self) -> RefreshRequest {
if self.running {
self.dirty = true;
+60 -78
View File
@@ -4,7 +4,7 @@ use std::path::PathBuf;
use anyhow::{Error, bail};
use bitcoin_hashes::sha1::Hash as Sha1Hash;
use gpui::{App, AppContext, AsyncApp, Context, SharedString, Subscription, Task, WeakEntity};
use gpui::{App, AppContext, Context, SharedString, Subscription, Task};
use nostr::event::IntoEventBuilder;
use nostr_sdk::prelude::*;
use settings::{EventFetchingStrategy, SettingsStore};
@@ -13,6 +13,7 @@ use signed_core::{
pull_request_patches,
};
use signed_git::Nip34Binding;
use signed_nostr::UniversalSigner;
use crate::backend::{
Backend, BackendEvent, grasp_base_url, grasp06_prs_url, pr_clone_urls, require_relay_accepted,
@@ -20,7 +21,6 @@ use crate::backend::{
};
use crate::checkouts::CheckoutsStore;
use crate::git_store::ensure_repo_mirror;
use crate::refresh::{RefreshGate, RefreshRequest};
use crate::repos::RepoListStore;
/// Maximum size of one patch event.
@@ -31,7 +31,6 @@ const MAX_PATCH_EVENT_BYTES: usize = 60 * 1024;
/// Per-repository store.
///
/// Holds the announcement, state, issues, patches, PRs, comments and resolved statuses.
/// Always derived from the local database.
pub struct RepoStore {
/// NIP-34 address. `None` while the repository is local-only.
addr: Option<RepoAddr>,
@@ -89,7 +88,8 @@ pub struct RepoStore {
///
/// Avoids re-running the maintainer Auto sync on every refresh.
synced_maintainers: HashSet<PublicKey>,
refresh: RefreshGate,
/// In-flight tasks, cancelled when the store drops.
tasks: Vec<Task<Result<(), Error>>>,
/// Backend subscription of an announced repository. `None` while local-only.
_subscription: Option<Subscription>,
}
@@ -138,7 +138,7 @@ impl RepoStore {
repo_relays: HashSet::new(),
root_fetches: HashSet::new(),
synced_maintainers: HashSet::new(),
refresh: RefreshGate::default(),
tasks: Vec::new(),
_subscription: Some(subscription),
}
}
@@ -167,7 +167,7 @@ impl RepoStore {
repo_relays: HashSet::new(),
root_fetches: HashSet::new(),
synced_maintainers: HashSet::new(),
refresh: RefreshGate::default(),
tasks: Vec::new(),
_subscription: None,
}
}
@@ -365,11 +365,6 @@ impl RepoStore {
if self.addr.is_none() {
return;
}
if self.refresh.request() != RefreshRequest::Schedule {
return;
}
self.run_refresh(cx);
}
@@ -378,8 +373,6 @@ impl RepoStore {
return;
};
self.refresh.begin();
let backend = Backend::global(cx);
let client = backend.read(cx).client();
@@ -498,7 +491,7 @@ impl RepoStore {
))
});
cx.spawn(async move |this, cx| {
let task = cx.spawn(async move |this, cx| {
let (
announcement,
state,
@@ -513,14 +506,13 @@ impl RepoStore {
Ok(data) => data,
Err(e) => {
return this.update(cx, |this, cx| {
this.refresh.abort();
this.last_error = Some(e.to_string());
cx.notify();
});
}
};
let again = this.update(cx, |this, cx| {
this.update(cx, |this, cx| {
let keep_hint = announcement.is_none() && !this.loaded;
let first_pass = !this.loaded;
@@ -604,17 +596,12 @@ impl RepoStore {
if changed {
cx.notify();
}
this.refresh.finish()
})?;
if again {
this.update(cx, |this, cx| this.refresh(cx))?;
}
Ok(())
})
.detach();
});
self.tasks.push(task);
}
/// Resolve the status of a root event, an issue, patch or PR, per NIP-34.
@@ -747,9 +734,12 @@ impl RepoStore {
};
let backend = Backend::global(cx);
let signer = backend.read(cx).signer();
let (signer, client, user) = {
let backend = backend.read(cx);
(backend.signer(), backend.client(), backend.current_user())
};
let Some(user) = backend.read(cx).current_user() else {
let Some(user) = user else {
self.last_error = Some("Sign in to open a pull request".into());
cx.notify();
return;
@@ -784,12 +774,12 @@ impl RepoStore {
.collect()
};
cx.spawn(async move |this, cx| {
// The PR references the root patch event.
// Viewers can then find the patch without carrying it inline.
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
// The PR references the root patch,
// viewers can then find the patch without carrying it inline.
let root_patch = match publish_patch_series(
&this,
cx,
&client,
&signer,
&addr,
owner,
euc.as_deref(),
@@ -814,11 +804,14 @@ impl RepoStore {
// Resolve the servers from the author's latest kind-10317 grasp list.
// The settings defaults stand in when no list is published or the query fails.
let author_servers = {
let query = this.update(cx, |_this, cx| {
let client = Backend::global(cx).read(cx).client();
user_grasp_list_servers(client, user)
})?;
match cx.background_spawn(query).await {
let query_client = client.clone();
let published = cx
.background_spawn(
async move { user_grasp_list_servers(&query_client, user).await },
)
.await;
match published {
Ok(published) if !published.is_empty() => published,
_ => defaults,
}
@@ -852,24 +845,17 @@ impl RepoStore {
};
let builder = this.update(cx, |this, _cx| {
// NIP-34 PRs carry at least one clone URL.
// The tip commit is downloadable from it.
// The author's `/prs/` URLs come first.
// They are author-controlled and most likely alive.
// The announced mirrors follow.
// The list is fixed before signing.
// The pushed ref name embeds the event id.
// Every candidate URL is listed up front.
// Dead URLs are inert, the linked patch stays the source of truth.
let prs_urls: Vec<Url> = author_targets
.iter()
.filter_map(|(url, _)| Url::parse(url).ok())
.collect();
let base_clone = this
.announcement
.as_ref()
.map(|a| a.clone.clone())
.unwrap_or_default();
let clone = pr_clone_urls(prs_urls, base_clone);
let builder = GitPullRequest {
@@ -894,8 +880,6 @@ impl RepoStore {
})?;
// Sign before publishing.
// The tip is pushed to the grasp servers under `refs/nostr/<event-id>`.
// Nak's convention, readers fetch that ref for the commit behind the `c` tag.
let event = cx
.background_spawn({
let signer = signer.clone();
@@ -906,20 +890,22 @@ impl RepoStore {
if let Some(path) = push_from.as_ref() {
let tip = current_commit.to_string();
let reference = format!("refs/nostr/{}", event.id.to_hex());
let (pushed, failures) = cx
.background_spawn({
let path = path.clone();
let tip = tip.clone();
let reference = reference.clone();
// Author servers first, then the announced base grasp servers.
// The extra targets are best-effort redundancy.
let targets: Vec<(String, String)> = author_targets
.into_iter()
.chain(base_targets)
.collect();
async move {
let mut failures = Vec::new();
let mut pushed = 0;
for (url, label) in &targets {
match signed_git::push_commit_ref(
&path, url, &tip, &reference,
@@ -928,6 +914,7 @@ impl RepoStore {
Err(e) => failures.push(format!("{label}: {e}")),
}
}
(pushed, failures)
}
})
@@ -944,8 +931,6 @@ impl RepoStore {
}
}
let client = this.update(cx, |_this, cx| Backend::global(cx).read(cx).client())?;
let publish_result: Result<Event, Error> = async {
let output = client.send_event(&event).broadcast().await?;
require_relay_accepted(output, event)
@@ -971,8 +956,8 @@ impl RepoStore {
}
Ok(())
})
.detach();
});
self.tasks.push(task);
}
/// Generate the patch between `merge_base` and `compare_ref` in `repo_path`,
@@ -1036,8 +1021,12 @@ impl RepoStore {
self.last_warning = None;
let backend = Backend::global(cx);
let (user, client, signer) = {
let backend = backend.read(cx);
(backend.current_user(), backend.client(), backend.signer())
};
let Some(user) = backend.read(cx).current_user() else {
let Some(user) = user else {
self.last_error = Some("Sign in to update the pull request".into());
cx.notify();
return;
@@ -1091,19 +1080,21 @@ impl RepoStore {
self.not_announced(cx);
return;
};
let owner = addr.public_key;
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
let root = root.clone();
let clone: Vec<Url> = self
.announcement
.as_ref()
.map(|a| a.clone.clone())
.unwrap_or_default();
cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
if let Err(e) = publish_patch_series(
&this,
cx,
&client,
&signer,
&addr,
owner,
euc.as_deref(),
@@ -1119,7 +1110,7 @@ impl RepoStore {
});
}
let builder = this.update(cx, |_this, _cx| {
let builder = {
let builder = GitPullRequestUpdate {
repository: addr.clone(),
pull_request_event: root.id,
@@ -1136,13 +1127,7 @@ impl RepoStore {
Some(euc) => builder.tag(Tag::parse(["r", euc]).expect("valid r tag")),
None => builder,
}
})?;
let (client, signer) = this.update(cx, |_this, cx| {
let backend = Backend::global(cx);
let backend = backend.read(cx);
(backend.client(), backend.signer())
})?;
};
let publish_result: Result<Event, Error> = async {
let event = builder.finalize_async(&signer).await?;
@@ -1159,8 +1144,8 @@ impl RepoStore {
}
Ok(())
})
.detach();
});
self.tasks.push(task);
}
/// Set the status of a root event.
@@ -1286,7 +1271,7 @@ impl RepoStore {
}
Ok(())
});
task.detach();
self.tasks.push(task);
}
/// The latest announcement of this repository,
@@ -1602,7 +1587,7 @@ impl RepoStore {
Ok(())
});
task.detach();
self.tasks.push(task);
}
}
@@ -1672,8 +1657,8 @@ fn patch_current_commit(patch: &str) -> Option<&str> {
/// Returns the root event, the one a PR references.
#[allow(clippy::too_many_arguments)]
async fn publish_patch_series(
this: &WeakEntity<RepoStore>,
cx: &mut AsyncApp,
client: &Client,
signer: &UniversalSigner,
addr: &RepoAddr,
owner: PublicKey,
euc: Option<&str>,
@@ -1681,12 +1666,6 @@ async fn publish_patch_series(
first_marker: &str,
reply_to: Option<EventId>,
) -> Result<Event, Error> {
let (client, signer) = this.update(cx, |_this, cx| {
let backend = Backend::global(cx);
let backend = backend.read(cx);
(backend.client(), backend.signer())
})?;
let mut root: Option<Event> = None;
let mut previous = reply_to;
@@ -1699,6 +1678,7 @@ async fn publish_patch_series(
};
let mut tags = vec![Tag::coordinate(addr.clone(), None), Tag::public_key(owner)];
if ix == 0 {
if let Ok(tag) = Tag::parse(["t", first_marker]) {
tags.push(tag);
@@ -1713,21 +1693,23 @@ async fn publish_patch_series(
{
tags.push(tag);
}
if let Some(euc) = euc
&& let Ok(tag) = Tag::parse(["r", euc])
{
tags.push(tag);
}
if let Ok(tag) = Tag::parse(["commit", commit]) {
tags.push(tag);
}
if let Ok(tag) = Tag::parse(["r", commit]) {
tags.push(tag);
}
let builder = EventBuilder::new(Kind::GitPatch, part.clone()).tags(tags);
let event = builder.finalize_async(&signer).await?;
let event = builder.finalize_async(signer).await?;
let output = client.send_event(&event).broadcast().await?;
let event = require_relay_accepted(output, event)?;
@@ -303,6 +303,7 @@ pub struct CommitDiffView {
loading: bool,
error: Option<SharedString>,
pane: Entity<DiffPane>,
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
}
impl CommitDiffView {
@@ -336,6 +337,7 @@ impl CommitDiffView {
loading: true,
error: None,
pane,
tasks: Vec::new(),
}
}
@@ -383,7 +385,7 @@ impl CommitDiffView {
Ok(())
});
task.detach();
self.tasks.push(task);
}
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
+6 -2
View File
@@ -187,12 +187,16 @@ impl InboxView {
self.refresh.begin();
let backend = Backend::global(cx);
let Some(me) = backend.read(cx).current_user() else {
let (me, client) = {
let backend = backend.read(cx);
(backend.current_user(), backend.client())
};
let Some(me) = me else {
self.refresh.abort();
return;
};
let client = backend.read(cx).client();
let state = self.state.clone();
let work = cx.background_spawn(async move { query_inbox(&client, me, &state).await });
@@ -79,8 +79,7 @@ pub struct PullRequestDetailView {
pane: Entity<DiffPane>,
commit_item_sizes: Rc<Vec<Size<Pixels>>>,
commit_scroll_handle: VirtualListScrollHandle,
/// The dock caches item panels, so without this observer a panel opened
/// before the store loaded would stay on its placeholder.
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
_subscription: Subscription,
}
@@ -124,6 +123,7 @@ impl PullRequestDetailView {
pane,
commit_item_sizes: Rc::new(Vec::new()),
commit_scroll_handle: VirtualListScrollHandle::new(),
tasks: Vec::new(),
_subscription: subscription,
}
}
@@ -327,7 +327,7 @@ impl PullRequestDetailView {
Ok(())
});
task.detach();
self.tasks.push(task);
}
/// Open the diff of `commit_id` in the bottom dock of the area.
@@ -68,6 +68,7 @@ pub struct NewPullRequestView {
pane: Entity<DiffPane>,
scroll_handle: VirtualListScrollHandle,
item_sizes: Rc<Vec<Size<Pixels>>>,
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
_subscriptions: Vec<Subscription>,
}
@@ -323,6 +324,7 @@ impl NewPullRequestView {
pane,
scroll_handle: VirtualListScrollHandle::new(),
item_sizes: Rc::new(Vec::new()),
tasks: Vec::new(),
_subscriptions: subscriptions,
}
}
@@ -390,7 +392,8 @@ impl NewPullRequestView {
Ok(())
});
task.detach();
self.tasks.push(task);
}
/// Branches and the current branch are read off the UI thread, then applied.
@@ -419,7 +422,8 @@ impl NewPullRequestView {
Ok(())
});
task.detach();
self.tasks.push(task);
}
fn apply_checkout(
@@ -636,7 +640,8 @@ impl NewPullRequestView {
Ok(())
});
task.detach();
self.tasks.push(task);
}
#[allow(clippy::too_many_arguments)]
@@ -830,7 +835,7 @@ impl NewPullRequestView {
Ok(())
});
task.detach();
self.tasks.push(task);
}
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -911,7 +916,7 @@ impl NewPullRequestView {
Ok(())
});
task.detach();
self.tasks.push(task);
}
fn open_commit_diff(&mut self, commit_id: &str, window: &mut Window, cx: &mut Context<Self>) {
@@ -215,15 +215,19 @@ pub fn load_user_grasp_servers(
cx: &mut App,
) {
let backend = Backend::global(cx);
let Some(user) = backend.read(cx).current_user() else {
let (user, client) = {
let backend = backend.read(cx);
(backend.current_user(), backend.client())
};
let Some(user) = user else {
state.update(cx, |state, _| state.loading_servers = false);
return;
};
let client = backend.read(cx).client();
let handle = window.window_handle();
cx.spawn(async move |cx| {
let result = signed_state::user_grasp_list_servers(client, user).await;
let result = signed_state::user_grasp_list_servers(&client, user).await;
let _ = cx.update_window(handle, |_, _window, cx| {
state.update(cx, |state, _| {