refactor
This commit is contained in:
@@ -160,6 +160,14 @@ impl Backend {
|
||||
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.
|
||||
@@ -180,7 +188,7 @@ impl Backend {
|
||||
Ok::<(), Error>(())
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(()) => {
|
||||
this.update(cx, |_this, cx| cx.notify())?;
|
||||
@@ -208,7 +216,7 @@ impl Backend {
|
||||
|
||||
let user = cx.read_credentials(USER_KEYRING);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
let content = match user.await {
|
||||
Ok(Some((_username, secret))) => String::from_utf8(secret)?,
|
||||
_ => {
|
||||
@@ -906,7 +914,7 @@ impl Backend {
|
||||
let pubkey = keys.public_key().to_hex();
|
||||
let write = cx.write_credentials(USER_KEYRING, &pubkey, nsec.as_bytes());
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_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(());
|
||||
@@ -932,7 +940,7 @@ impl Backend {
|
||||
let credential = with_master_key(&uri_string, &keys);
|
||||
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
let result = async {
|
||||
let mut signer = NostrConnect::new(
|
||||
connect_uri,
|
||||
@@ -964,7 +972,7 @@ impl Backend {
|
||||
pub fn logout(&mut self, cx: &mut Context<Self>) {
|
||||
let delete = cx.delete_credentials(USER_KEYRING);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
delete.await.ok();
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -984,7 +992,7 @@ impl Backend {
|
||||
fn bootstrap_user(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||
let client = self.client.clone();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
let result = async {
|
||||
let events: Vec<Event> = client
|
||||
.fetch_events(filters::grasp_list(public_key))
|
||||
@@ -1068,7 +1076,7 @@ impl Backend {
|
||||
|
||||
Ok(())
|
||||
});
|
||||
self.tasks.push(task);
|
||||
self.push_task(task);
|
||||
}
|
||||
|
||||
/// Add relays and connect to them.
|
||||
@@ -1083,7 +1091,7 @@ impl Backend {
|
||||
Ok::<(), Error>(())
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(()) => {
|
||||
this.update(cx, |_this, cx| cx.notify())?;
|
||||
@@ -1125,7 +1133,7 @@ impl Backend {
|
||||
|
||||
let client = self.client.clone();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
if let Err(e) = connect_repo_relays_only(&client, relays, filters).await {
|
||||
log::warn!("repo relay fetch failed: {e}");
|
||||
// Allow an immediate retry after a failure.
|
||||
@@ -1145,7 +1153,7 @@ impl Backend {
|
||||
let task =
|
||||
cx.background_spawn(async move { subscribe_bootstrap_only(&client, filters).await });
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
if let Err(e) = task.await {
|
||||
this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?;
|
||||
}
|
||||
@@ -1168,7 +1176,7 @@ impl Backend {
|
||||
|
||||
let (tx, mut rx) = SyncProgress::channel();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
let mut last_percent: u64 = 0;
|
||||
|
||||
while rx.changed().await.is_ok() {
|
||||
@@ -1201,7 +1209,7 @@ impl Backend {
|
||||
sync_bootstrap_only(&client, filter, opts).await
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(summary) => {
|
||||
log::debug!(
|
||||
@@ -1287,7 +1295,7 @@ impl Backend {
|
||||
fn send_fire_and_forget(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
|
||||
let task = self.send(builder, cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
if let Err(e) = task.await {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(BackendEvent::error(e.to_string()));
|
||||
@@ -1313,7 +1321,7 @@ impl Backend {
|
||||
|
||||
let task = self.send(EventBuilder::new(Kind::EventDeletion, "").tags(tags), cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, _cx| {
|
||||
self.push_task(cx.spawn(async move |_this, _cx| {
|
||||
if let Err(e) = task.await {
|
||||
log::warn!("failed to retract repository events: {e}");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::Error;
|
||||
@@ -67,9 +66,9 @@ struct Remembered {
|
||||
/// Global store of local-checkout associations and per-checkout statuses.
|
||||
pub struct CheckoutsStore {
|
||||
/// Checkout paths per announced repository.
|
||||
by_repo: Arc<HashMap<RepoAddr, Vec<PathBuf>>>,
|
||||
by_repo: HashMap<RepoAddr, Vec<PathBuf>>,
|
||||
/// Ready-to-contribute statuses of the requested repositories.
|
||||
statuses: Arc<HashMap<RepoAddr, Vec<CheckoutStatus>>>,
|
||||
statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>,
|
||||
/// Repositories whose statuses are recomputed on every input change.
|
||||
///
|
||||
/// Those are the repository detail panels currently open.
|
||||
@@ -79,7 +78,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.
|
||||
push_statuses: Arc<HashMap<RepoAddr, Vec<CheckoutStatus>>>,
|
||||
push_statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>,
|
||||
/// Last announced head branch per requested repository.
|
||||
///
|
||||
/// A recompute defaults the base the same way.
|
||||
@@ -129,19 +128,19 @@ impl CheckoutsStore {
|
||||
this.status_requested.clear();
|
||||
this.push_requested.clear();
|
||||
this.requested_head.clear();
|
||||
this.statuses = Arc::new(HashMap::new());
|
||||
this.push_statuses = Arc::new(HashMap::new());
|
||||
this.statuses = HashMap::new();
|
||||
this.push_statuses = HashMap::new();
|
||||
this.refresh(cx);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let mut store = Self {
|
||||
by_repo: Arc::new(HashMap::new()),
|
||||
statuses: Arc::new(HashMap::new()),
|
||||
by_repo: HashMap::new(),
|
||||
statuses: HashMap::new(),
|
||||
status_requested: HashSet::new(),
|
||||
push_requested: HashSet::new(),
|
||||
push_statuses: Arc::new(HashMap::new()),
|
||||
push_statuses: HashMap::new(),
|
||||
requested_head: HashMap::new(),
|
||||
refresh: RefreshGate::default(),
|
||||
_subscriptions: subscriptions,
|
||||
@@ -155,6 +154,14 @@ 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") {
|
||||
@@ -243,7 +250,7 @@ impl CheckoutsStore {
|
||||
this.update(cx, |this, cx| this.run_refresh(cx))
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
self.push_task(task);
|
||||
}
|
||||
|
||||
/// One resolve and apply cycle, the debounced entry point.
|
||||
@@ -345,7 +352,7 @@ impl CheckoutsStore {
|
||||
Ok::<_, Error>((associations, statuses, push_statuses))
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
let (associations, statuses, push_statuses) = match work.await {
|
||||
Ok(results) => results,
|
||||
Err(_) => {
|
||||
@@ -357,9 +364,9 @@ impl CheckoutsStore {
|
||||
};
|
||||
|
||||
let again = this.update(cx, |this, cx| {
|
||||
this.by_repo = Arc::new(associations);
|
||||
this.statuses = Arc::new(statuses);
|
||||
this.push_statuses = Arc::new(push_statuses);
|
||||
this.by_repo = associations;
|
||||
this.statuses = statuses;
|
||||
this.push_statuses = push_statuses;
|
||||
cx.notify();
|
||||
|
||||
this.refresh.finish()
|
||||
@@ -386,7 +393,7 @@ impl CheckoutsStore {
|
||||
this.update(cx, |this, cx| this.run_refresh(cx))
|
||||
});
|
||||
|
||||
this.tasks.push(task);
|
||||
this.push_task(task);
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ pub struct GitStore(GitCache);
|
||||
|
||||
impl GitStore {
|
||||
/// Register the clone cache rooted at `root` as an app-wide global.
|
||||
///
|
||||
/// Replaces any installed store, [`signed_state::init`] installs an empty one.
|
||||
pub fn set_global(root: impl Into<PathBuf>, cx: &mut App) -> Self {
|
||||
let store = Self::new(root);
|
||||
cx.set_global(GlobalGitStore(store.0.clone()));
|
||||
|
||||
@@ -23,7 +23,12 @@ use signed_nostr::new_backend;
|
||||
/// Initialize the backend and stores, and install them as globals.
|
||||
/// Call once at startup, before opening any window that uses the stores.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn init(db_path: impl AsRef<Path>, scan_paths: Vec<PathBuf>, cx: &mut App) -> Entity<Backend> {
|
||||
pub fn init(
|
||||
db_path: impl AsRef<Path>,
|
||||
repos_root: impl Into<PathBuf>,
|
||||
scan_paths: Vec<PathBuf>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Backend> {
|
||||
// rustls uses the `aws_lc_rs` provider by default.
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
@@ -41,7 +46,8 @@ pub fn init(db_path: impl AsRef<Path>, scan_paths: Vec<PathBuf>, cx: &mut App) -
|
||||
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
||||
|
||||
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
|
||||
GitStore::set_global(PathBuf::new(), cx);
|
||||
// The local git clone cache, the grasp mirrors.
|
||||
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);
|
||||
|
||||
|
||||
@@ -3,8 +3,11 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Error;
|
||||
use flume::{Receiver, RecvTimeoutError, Sender};
|
||||
use gpui::{App, AppContext, Context, Entity, Global, SharedString, Subscription, Task};
|
||||
use flume::{Receiver, Sender};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, Global, SharedString, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use utils::shorten_pubkey;
|
||||
|
||||
@@ -60,14 +63,6 @@ impl Profile {
|
||||
}
|
||||
}
|
||||
|
||||
/// Message from the fetch task to the main thread.
|
||||
enum Dispatch {
|
||||
/// A batched sync finished.
|
||||
///
|
||||
/// Re-read seen profiles from the database.
|
||||
Synced,
|
||||
}
|
||||
|
||||
/// How long to wait for more requests before firing a batched sync.
|
||||
const BATCH_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
|
||||
@@ -114,23 +109,15 @@ impl ProfileStore {
|
||||
_ => {}
|
||||
});
|
||||
|
||||
// Fetch requests are queued on a channel.
|
||||
// Fetch requests are queued on a channel, batched into one sync per debounce window.
|
||||
let client = backend.read(cx).client();
|
||||
let (sender, receiver) = flume::unbounded::<PublicKey>();
|
||||
let (dispatch_tx, dispatch_rx) = flume::unbounded::<Dispatch>();
|
||||
let entity = cx.entity().downgrade();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
tasks.push(cx.background_spawn(async move {
|
||||
Self::handle_requests(&client, &dispatch_tx, &receiver).await
|
||||
}));
|
||||
|
||||
// Re-read seen profiles from the database after each batch sync.
|
||||
tasks.push(cx.spawn(async move |this, cx| {
|
||||
while let Ok(Dispatch::Synced) = dispatch_rx.recv_async().await {
|
||||
this.update(cx, |this, cx| this.apply_seen(cx)).ok();
|
||||
}
|
||||
Ok(())
|
||||
tasks.push(cx.spawn(async move |_this, cx| {
|
||||
Self::handle_requests(entity, &client, &receiver, cx).await
|
||||
}));
|
||||
|
||||
let mut store = Self {
|
||||
@@ -145,6 +132,14 @@ 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.
|
||||
@@ -186,7 +181,7 @@ impl ProfileStore {
|
||||
Ok::<_, Error>(profiles)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
let profiles = work.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -222,7 +217,7 @@ impl ProfileStore {
|
||||
Ok::<_, Error>(profile)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
let profile = work.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -278,7 +273,7 @@ impl ProfileStore {
|
||||
Ok::<_, Error>(profiles)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(cx.spawn(async move |this, cx| {
|
||||
let profiles = work.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -294,28 +289,43 @@ impl ProfileStore {
|
||||
|
||||
/// Sync metadata for requested authors in batches, debounced to collect requests.
|
||||
///
|
||||
/// Results are dispatched to the main thread, which re-reads the database.
|
||||
/// After each batch, the seen profiles are re-read from the database on the main thread.
|
||||
async fn handle_requests(
|
||||
this: WeakEntity<ProfileStore>,
|
||||
client: &Client,
|
||||
dispatch: &Sender<Dispatch>,
|
||||
receiver: &Receiver<PublicKey>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(), Error> {
|
||||
let mut batch: HashSet<PublicKey> = HashSet::new();
|
||||
|
||||
loop {
|
||||
// Wait for the first request of a batch.
|
||||
match receiver.recv_timeout(BATCH_TIMEOUT) {
|
||||
match receiver.recv_async().await {
|
||||
Ok(public_key) => {
|
||||
batch.insert(public_key);
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => return Ok(()),
|
||||
Err(RecvTimeoutError::Timeout) => continue,
|
||||
};
|
||||
Err(_) => return Ok(()),
|
||||
}
|
||||
|
||||
// Collect everything that arrives within the debounce window.
|
||||
// The channel has no async timeout, race the receive against a timer.
|
||||
let deadline = Instant::now() + BATCH_TIMEOUT;
|
||||
while let Ok(public_key) = receiver.recv_deadline(deadline) {
|
||||
batch.insert(public_key);
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
if now >= deadline {
|
||||
break;
|
||||
}
|
||||
let timer = cx.background_executor().timer(deadline - now);
|
||||
futures::pin_mut!(timer);
|
||||
let recv = receiver.recv_async();
|
||||
futures::pin_mut!(recv);
|
||||
match futures::future::select(recv, timer).await {
|
||||
futures::future::Either::Left((Ok(public_key), _)) => {
|
||||
batch.insert(public_key);
|
||||
}
|
||||
futures::future::Either::Left((Err(_), _)) => return Ok(()),
|
||||
futures::future::Either::Right(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
let filter = Filter::new()
|
||||
@@ -327,9 +337,7 @@ impl ProfileStore {
|
||||
// Re-apply from the database afterwards.
|
||||
match sync_bootstrap_only(client, filter, SyncOptions::default()).await {
|
||||
Ok(_) => {
|
||||
if dispatch.send(Dispatch::Synced).is_err() {
|
||||
log::warn!("profile dispatch channel closed, dropping sync result");
|
||||
}
|
||||
let _ = this.update(cx, |this, cx| this.apply_seen(cx));
|
||||
}
|
||||
Err(e) => log::warn!("profile sync failed: {e}"),
|
||||
}
|
||||
|
||||
@@ -129,6 +129,14 @@ impl RepoListStore {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Negentropy-sync announcements with the bootstrap relays.
|
||||
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
||||
let backend = Backend::global(cx);
|
||||
@@ -170,7 +178,7 @@ impl RepoListStore {
|
||||
this.update(cx, |this, cx| this.run_refresh(cx))
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
self.push_task(task);
|
||||
}
|
||||
|
||||
/// One query and apply cycle, the debounced entry point.
|
||||
@@ -291,7 +299,7 @@ impl RepoListStore {
|
||||
Ok::<_, Error>((announcements, last_activity, counts))
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
self.push_task(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.
|
||||
|
||||
Reference in New Issue
Block a user