This commit is contained in:
2026-09-04 16:02:51 +07:00
parent 17de4f6376
commit 1d224218df
38 changed files with 894 additions and 2482 deletions
+42 -134
View File
@@ -1,5 +1,6 @@
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;
@@ -45,8 +46,6 @@ pub enum BackendEvent {
PassphraseRequired,
/// The signer changed on login, logout or account switch.
SignerChanged,
/// Relay bootstrap finished.
Connected,
/// A new event was received from a relay and stored in the database.
NostrUpdate(Update),
/// A negentropy sync completed.
@@ -80,7 +79,6 @@ pub struct Backend {
client: Client,
signer: UniversalSigner,
current_user: Option<PublicKey>,
connected: bool,
sync_progress: Option<(u64, u64)>,
/// True when the stored credential is NIP-49 encrypted.
passphrase_required: bool,
@@ -151,7 +149,6 @@ impl Backend {
client,
signer,
current_user: None,
connected: false,
sync_progress: None,
passphrase_required: false,
recent_fetches: HashMap::new(),
@@ -186,11 +183,7 @@ impl Backend {
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(()) => {
this.update(cx, |this, cx| {
this.connected = true;
cx.emit(BackendEvent::Connected);
cx.notify();
})?;
this.update(cx, |_this, cx| cx.notify())?;
}
Err(e) => {
this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?;
@@ -993,22 +986,14 @@ impl Backend {
self.tasks.push(cx.spawn(async move |this, cx| {
let result = async {
let events = client.fetch_events(filters::grasp_list(public_key)).await?;
let urls: Vec<String> = events
let events: Vec<Event> = client
.fetch_events(filters::grasp_list(public_key))
.await?
.into_iter()
.max_by_key(|e| e.created_at)
.map(|e| {
e.tags
.iter()
.filter(|t| t.kind() == "g")
.filter_map(|t| t.content().map(str::to_owned))
.collect()
})
.unwrap_or_default();
.collect();
for url in urls {
client.add_relay(&url).await.ok();
for url in latest_grasp_list_servers(events) {
client.add_relay(url.as_str()).await.ok();
}
client.connect().await;
@@ -1049,11 +1034,6 @@ impl Backend {
cx.emit(BackendEvent::error(message));
}
/// Whether the relay bootstrap has completed.
pub fn is_connected(&self) -> bool {
self.connected
}
/// Progress of the in-flight negentropy sync, if any.
pub fn sync_progress(&self) -> Option<(u64, u64)> {
self.sync_progress
@@ -1106,11 +1086,7 @@ impl Backend {
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(()) => {
this.update(cx, |this, cx| {
this.connected = true;
cx.emit(BackendEvent::Connected);
cx.notify();
})?;
this.update(cx, |_this, cx| cx.notify())?;
}
Err(e) => {
this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?;
@@ -1120,43 +1096,6 @@ impl Backend {
}));
}
/// Add discovery-only relays, e.g. NIP-65 indexers, and connect to them.
pub fn add_discovery_relays(&mut self, urls: Vec<String>, cx: &mut Context<Self>) {
let client = self.client.clone();
let task = cx.background_spawn(async move {
for url in urls {
client
.add_relay(&url)
.capabilities(RelayCapabilities::DISCOVERY)
.await?;
}
client.connect().await;
Ok::<(), Error>(())
});
self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?;
}
Ok(())
}));
}
/// Start a persistent subscription.
pub fn subscribe(&mut self, filter: Filter, cx: &mut Context<Self>) {
let client = self.client.clone();
let task = cx.background_spawn(async move { client.subscribe(filter).await.map(|_| ()) });
self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?;
}
Ok(())
}));
}
/// Whether an identical fetch started within [`FETCH_DEDUP_WINDOW`] is still recent.
///
/// Records the fingerprint when returning `false`, pruning expired entries first.
@@ -1298,44 +1237,11 @@ impl Backend {
let client = self.client.clone();
let signer = self.signer.clone();
cx.spawn(async move |this, cx| {
self.publish_task(cx, async move {
// Sign with the current signer, broadcast and save locally.
// The event is immediately visible to database queries.
let work = cx.background_spawn(async move {
let event = builder.finalize_async(&signer).await?;
let output = client.send_event(&event).await?;
if output.success.is_empty() && !output.failed.is_empty() {
let reasons = output
.failed
.values()
.cloned()
.collect::<Vec<String>>()
.join(", ");
return Err(anyhow!("event not accepted by any relay: {reasons}"));
}
Ok(event)
});
let result = work.await;
match &result {
Ok(event) => {
this.update(cx, |_this, cx| {
cx.emit(BackendEvent::Published(Box::new(event.clone())));
})
.ok();
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(BackendEvent::error(e.to_string()));
})
.ok();
}
}
result
let event = builder.finalize_async(&signer).await?;
broadcast_event(&client, &event).await
})
}
@@ -1346,25 +1252,17 @@ impl Backend {
cx: &mut Context<Self>,
) -> Task<Result<Event, Error>> {
let client = self.client.clone();
self.publish_task(cx, async move { broadcast_event(&client, &event).await })
}
/// Run `work` in the background, then emit its outcome as a [`BackendEvent`].
fn publish_task(
&mut self,
cx: &mut Context<Self>,
work: impl Future<Output = Result<Event, Error>> + 'static + Send,
) -> Task<Result<Event, Error>> {
cx.spawn(async move |this, cx| {
let work = cx.background_spawn(async move {
let output = client.send_event(&event).await?;
if output.success.is_empty() && !output.failed.is_empty() {
let reasons = output
.failed
.values()
.cloned()
.collect::<Vec<String>>()
.join(", ");
return Err(anyhow!("event not accepted by any relay: {reasons}"));
}
Ok(event.clone())
});
let result = work.await;
let result = cx.background_spawn(work).await;
match &result {
Ok(event) => {
@@ -1385,15 +1283,6 @@ impl Backend {
})
}
/// Publish a NIP-34 repository announcement, kind 30617, with the current signer.
pub fn publish_announcement(
&mut self,
announcement: GitRepositoryAnnouncement,
cx: &mut Context<Self>,
) -> Task<Result<Event, Error>> {
self.send(announcement.into_event_builder(), cx)
}
/// 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);
@@ -1433,6 +1322,25 @@ impl Backend {
}
}
/// Broadcast an event and fail when no relay accepted it.
///
/// The client stores accepted events locally, visible to database queries.
async fn broadcast_event(client: &Client, event: &Event) -> Result<Event, Error> {
let output = client.send_event(event).await?;
if output.success.is_empty() && !output.failed.is_empty() {
let reasons = output
.failed
.values()
.cloned()
.collect::<Vec<String>>()
.join(", ");
return Err(anyhow!("event not accepted by any relay: {reasons}"));
}
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.
@@ -1603,8 +1511,8 @@ fn latest_grasp_list_servers(events: Vec<Event>) -> Vec<RelayUrl> {
.unwrap_or_default()
}
/// Resolve the user's published grasp servers.
pub(crate) async fn user_grasp_list_servers(
/// Resolve the user's published grasp servers from the local database.
pub async fn user_grasp_list_servers(
client: Client,
user: PublicKey,
) -> Result<Vec<RelayUrl>, Error> {