feat: improve performance (#234)

* feat: use negentropy as much as possible

* update

* update
This commit is contained in:
雨宮蓮
2024-09-29 16:53:39 +07:00
committed by GitHub
parent afa9327bb7
commit f0fc89724d
26 changed files with 566 additions and 373 deletions

View File

@@ -3,7 +3,12 @@ use keyring_search::{Limit, List, Search};
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::{collections::HashSet, str::FromStr, time::Duration};
use std::{
collections::HashSet,
fs::{self, File},
str::FromStr,
time::Duration,
};
use tauri::{Emitter, Manager, State};
use crate::{
@@ -133,7 +138,7 @@ pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<Str
let remote_user = bunker_uri.signer_public_key().unwrap();
let remote_npub = remote_user.to_bech32().unwrap();
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None) {
Ok(signer) => {
let mut url = Url::parse(&uri).unwrap();
let query: Vec<(String, String)> = url
@@ -204,6 +209,17 @@ pub fn delete_account(id: String) -> Result<(), String> {
Ok(())
}
#[tauri::command]
#[specta::specta]
pub fn is_account_sync(id: String, handle: tauri::AppHandle) -> bool {
let config_dir = handle
.path()
.app_config_dir()
.expect("Error: app config directory not found.");
fs::metadata(config_dir.join(id)).is_ok()
}
#[tauri::command]
#[specta::specta]
pub async fn login(
@@ -244,7 +260,7 @@ pub async fn login(
let public_key = uri.signer_public_key().unwrap().to_bech32().unwrap();
let app_keys = Keys::from_str(&account.password).map_err(|e| e.to_string())?;
match Nip46Signer::new(uri, app_keys, Duration::from_secs(120), None).await {
match Nip46Signer::new(uri, app_keys, Duration::from_secs(120), None) {
Ok(signer) => {
// Update signer
client.set_signer(Some(signer.into())).await;
@@ -259,6 +275,11 @@ pub async fn login(
init_nip65(client, &public_key).await;
tauri::async_runtime::spawn(async move {
let config_dir = handle
.path()
.app_config_dir()
.expect("Error: app config directory not found.");
let state = handle.state::<Nostr>();
let client = &state.client;
@@ -298,64 +319,85 @@ pub async fn login(
state.settings.lock().await.clone_from(&settings);
};
let contact_list = client
.get_contact_list(Some(Duration::from_secs(5)))
.await
.unwrap();
// Get user's contact list
if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await {
state.contact_list.lock().await.clone_from(&contacts);
if !contact_list.is_empty() {
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
if !contacts.is_empty() {
let pubkeys: Vec<PublicKey> = contacts.iter().map(|f| f.public_key).collect();
let metadata = Filter::new()
.authors(authors.clone())
.kind(Kind::Metadata)
.limit(authors.len());
let newsfeed = Filter::new()
.authors(pubkeys.clone())
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(NEWSFEED_NEG_LIMIT);
if let Ok(report) = client
.reconcile(metadata, NegentropyOptions::default())
.await
{
println!("received [metadata]: {}", report.received.len())
}
if client
.reconcile(newsfeed, NegentropyOptions::default())
.await
.is_ok()
{
handle.emit("synchronized", ()).unwrap();
}
let newsfeed = Filter::new()
.authors(authors.clone())
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(NEWSFEED_NEG_LIMIT);
if client
.reconcile(newsfeed, NegentropyOptions::default())
.await
.is_ok()
{
// Save state
let _ = File::create(config_dir.join(public_key.to_bech32().unwrap()));
// Update frontend
handle.emit("synchronized", ()).unwrap();
}
let contacts = Filter::new()
.authors(authors.clone())
.kind(Kind::ContactList)
.limit(authors.len() * 1000);
if let Ok(report) = client
.reconcile(contacts, NegentropyOptions::default())
.await
{
println!("received [contact list]: {}", report.received.len())
}
for author in authors.into_iter() {
let filter = Filter::new()
.authors(pubkeys.clone())
.author(author)
.kind(Kind::ContactList)
.limit(4000);
.limit(1);
if client
.reconcile(filter, NegentropyOptions::default())
.await
.is_ok()
{
for pubkey in pubkeys.into_iter() {
let mut list: Vec<PublicKey> = Vec::new();
let f = Filter::new()
.author(pubkey)
.kind(Kind::ContactList)
.limit(1);
let mut circles = state.circles.lock().await;
let mut list: Vec<PublicKey> = Vec::new();
if let Ok(events) = client.database().query(vec![f]).await {
if let Some(event) = events.into_iter().next() {
for tag in event.tags.into_iter() {
if let Some(TagStandard::PublicKey {
public_key,
uppercase: false,
..
}) = tag.to_standardized()
{
list.push(public_key)
}
}
if !list.is_empty() {
state.circles.lock().await.insert(pubkey, list);
};
if let Ok(events) = client.database().query(vec![filter]).await {
if let Some(event) = events.into_iter().next() {
for tag in event.tags.into_iter() {
if let Some(TagStandard::PublicKey {
public_key,
uppercase: false,
..
}) = tag.to_standardized()
{
list.push(public_key)
}
}
if !list.is_empty() {
circles.insert(author, list);
};
}
}
};
}
} else {
handle.emit("synchronized", ()).unwrap();
};
});

View File

@@ -28,13 +28,7 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
let filter = Filter::new().id(event_id);
match client
.get_events_of(
vec![filter],
EventSource::both(Some(Duration::from_secs(5))),
)
.await
{
match client.database().query(vec![filter.clone()]).await {
Ok(events) => {
if let Some(event) = events.first() {
let raw = event.as_json();
@@ -46,7 +40,29 @@ pub async fn get_event(id: String, state: State<'_, Nostr>) -> Result<RichEvent,
Ok(RichEvent { raw, parsed })
} else {
Err("Cannot found this event with current relay list".into())
match client
.get_events_of(
vec![filter],
EventSource::relays(Some(Duration::from_secs(5))),
)
.await
{
Ok(events) => {
if let Some(event) = events.first() {
let raw = event.as_json();
let parsed = if event.kind == Kind::TextNote {
Some(parse_event(&event.content).await)
} else {
None
};
Ok(RichEvent { raw, parsed })
} else {
Err("Cannot found this event with current relay list".into())
}
}
Err(err) => Err(err.to_string()),
}
}
}
Err(err) => Err(err.to_string()),
@@ -191,15 +207,12 @@ pub async fn get_events_by(
let author = PublicKey::parse(&public_key).map_err(|err| err.to_string())?;
let filter = Filter::new()
.kinds(vec![Kind::TextNote])
.kinds(vec![Kind::TextNote, Kind::Repost])
.author(author)
.limit(limit as usize);
match client
.get_events_of(
vec![filter],
EventSource::both(Some(Duration::from_secs(5))),
)
.get_events_of(vec![filter], EventSource::Database)
.await
{
Ok(events) => {
@@ -224,17 +237,11 @@ pub async fn get_events_by(
#[tauri::command]
#[specta::specta]
pub async fn get_events_from_contacts(
pub async fn get_local_events(
until: Option<&str>,
state: State<'_, Nostr>,
) -> Result<Vec<RichEvent>, String> {
let client = &state.client;
let contact_list = state.contact_list.lock().await;
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
if authors.is_empty() {
return Err("Contact List is empty.".into());
}
let as_of = match until {
Some(until) => Timestamp::from_str(until).map_err(|err| err.to_string())?,
@@ -244,8 +251,7 @@ pub async fn get_events_from_contacts(
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(FETCH_LIMIT)
.until(as_of)
.authors(authors);
.until(as_of);
match client.database().query(vec![filter]).await {
Ok(events) => {
@@ -295,7 +301,7 @@ pub async fn get_group_events(
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(FETCH_LIMIT)
.limit(20)
.until(as_of)
.authors(authors);
@@ -341,7 +347,7 @@ pub async fn get_global_events(
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(FETCH_LIMIT)
.limit(20)
.until(as_of);
match client
@@ -385,7 +391,7 @@ pub async fn get_hashtag_events(
};
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(FETCH_LIMIT)
.limit(20)
.until(as_of)
.hashtags(hashtags);

View File

@@ -46,10 +46,7 @@ pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<
.kind(Kind::Metadata)
.limit(1);
match client
.get_events_of(vec![filter], EventSource::Database)
.await
{
match client.database().query(vec![filter.clone()]).await {
Ok(events) => {
if let Some(event) = events.first() {
if let Ok(metadata) = Metadata::from_json(&event.content) {
@@ -58,7 +55,26 @@ pub async fn get_profile(id: Option<String>, state: State<'_, Nostr>) -> Result<
Err("Parse metadata failed".into())
}
} else {
Ok(Metadata::new().as_json())
match client
.get_events_of(
vec![filter],
EventSource::relays(Some(Duration::from_secs(5))),
)
.await
{
Ok(events) => {
if let Some(event) = events.first() {
if let Ok(metadata) = Metadata::from_json(&event.content) {
Ok(metadata.as_json())
} else {
Err("Parse metadata failed".into())
}
} else {
Ok(Metadata::new().as_json())
}
}
Err(e) => Err(e.to_string()),
}
}
}
Err(e) => Err(e.to_string()),
@@ -80,9 +96,6 @@ pub async fn set_contact_list(
})
.collect();
// Update local state
state.contact_list.lock().await.clone_from(&contact_list);
match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true),
Err(err) => Err(err.to_string()),
@@ -94,18 +107,13 @@ pub async fn set_contact_list(
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client;
match client.get_contact_list(Some(Duration::from_secs(10))).await {
match client.get_contact_list(Some(Duration::from_secs(5))).await {
Ok(contact_list) => {
if !contact_list.is_empty() {
let list = contact_list
.into_iter()
.map(|f| f.public_key.to_hex())
.collect();
Ok(list)
} else {
Err("Empty.".into())
}
let list = contact_list
.into_iter()
.map(|f| f.public_key.to_hex())
.collect();
Ok(list)
}
Err(err) => Err(err.to_string()),
}
@@ -144,16 +152,14 @@ pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result<St
}
}
#[tauri::command]
#[specta::specta]
pub async fn is_contact_list_empty(state: State<'_, Nostr>) -> Result<bool, ()> {
Ok(state.contact_list.lock().await.is_empty())
}
#[tauri::command]
#[specta::specta]
pub async fn check_contact(hex: String, state: State<'_, Nostr>) -> Result<bool, String> {
let contact_list = state.contact_list.lock().await;
let client = &state.client;
let contact_list = client
.get_contact_list(Some(Duration::from_secs(5)))
.await
.map_err(|e| e.to_string())?;
match PublicKey::parse(&hex) {
Ok(public_key) => match contact_list.iter().position(|x| x.public_key == public_key) {
@@ -190,9 +196,6 @@ pub async fn toggle_contact(
}
}
// Update local state
state.contact_list.lock().await.clone_from(&contact_list);
// Publish
match client.set_contact_list(contact_list).await {
Ok(event_id) => Ok(event_id.to_string()),

View File

@@ -13,7 +13,6 @@ use crate::Settings;
pub struct Meta {
pub content: String,
pub images: Vec<String>,
pub videos: Vec<String>,
pub events: Vec<String>,
pub mentions: Vec<String>,
pub hashtags: Vec<String>,
@@ -44,7 +43,6 @@ const NOSTR_MENTIONS: [&str; 10] = [
"Nostr:naddr1",
];
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
const VIDEOS: [&str; 5] = ["mp4", "mov", "avi", "webm", "mkv"];
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
events.iter().next()
@@ -115,7 +113,6 @@ pub async fn parse_event(content: &str) -> Meta {
.collect::<Vec<_>>();
let mut images = Vec::new();
let mut videos = Vec::new();
let mut text = content.to_string();
if !urls.is_empty() {
@@ -135,12 +132,6 @@ pub async fn parse_event(content: &str) -> Meta {
// Process the next item.
continue;
}
if VIDEOS.contains(&ext) {
text = text.replace(url_str, "");
videos.push(url_str.to_string());
// Process the next item.
continue;
}
}
// Check the content type of URL via HEAD request
@@ -167,7 +158,6 @@ pub async fn parse_event(content: &str) -> Meta {
mentions,
hashtags,
images,
videos,
}
}
@@ -262,7 +252,7 @@ pub async fn init_nip65(client: &Client, public_key: &str) {
if let Ok(events) = client
.get_events_of(
vec![filter],
EventSource::both(Some(Duration::from_secs(5))),
EventSource::relays(Some(Duration::from_secs(5))),
)
.await
{
@@ -284,8 +274,6 @@ pub async fn init_nip65(client: &Client, public_key: &str) {
}
}
}
} else {
eprintln!("Failed to get events for RelayList.");
}
}
@@ -344,7 +332,6 @@ mod tests {
assert_eq!(meta.content, "Check this image: #cool @npub1");
assert_eq!(meta.images, vec!["https://example.com/image.jpg"]);
assert_eq!(meta.videos, Vec::<String>::new());
assert_eq!(meta.hashtags, vec!["#cool"]);
assert_eq!(meta.mentions, vec!["@npub1"]);
}
@@ -356,7 +343,6 @@ mod tests {
assert_eq!(meta.content, "Check this video: #cool @npub1");
assert_eq!(meta.images, Vec::<String>::new());
assert_eq!(meta.videos, vec!["https://example.com/video.mp4"]);
assert_eq!(meta.hashtags, vec!["#cool"]);
assert_eq!(meta.mentions, vec!["@npub1"]);
}

View File

@@ -30,7 +30,6 @@ pub mod common;
pub struct Nostr {
client: Client,
contact_list: Mutex<Vec<Contact>>,
settings: Mutex<Settings>,
circles: Mutex<HashMap<PublicKey, Vec<PublicKey>>>,
}
@@ -56,7 +55,7 @@ impl Default for Settings {
image_resize_service: Some("https://wsrv.nl".to_string()),
use_relay_hint: true,
content_warning: true,
trusted_only: true,
trusted_only: false,
display_avatar: true,
display_zap_button: true,
display_repost_button: true,
@@ -83,8 +82,8 @@ struct Subscription {
struct NewSettings(Settings);
pub const DEFAULT_DIFFICULTY: u8 = 21;
pub const FETCH_LIMIT: usize = 20;
pub const NEWSFEED_NEG_LIMIT: usize = 256;
pub const FETCH_LIMIT: usize = 100;
pub const NEWSFEED_NEG_LIMIT: usize = 512;
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
@@ -107,12 +106,12 @@ fn main() {
get_private_key,
delete_account,
reset_password,
is_account_sync,
login,
get_profile,
set_profile,
get_contact_list,
set_contact_list,
is_contact_list_empty,
check_contact,
toggle_contact,
get_mention_list,
@@ -135,7 +134,7 @@ fn main() {
get_replies,
subscribe_to,
get_events_by,
get_events_from_contacts,
get_local_events,
get_group_events,
get_global_events,
get_hashtag_events,
@@ -222,9 +221,9 @@ fn main() {
.gossip(true)
.max_avg_latency(Duration::from_millis(500))
.automatic_authentication(false)
.connection_timeout(Some(Duration::from_secs(5)))
.send_timeout(Some(Duration::from_secs(5)))
.timeout(Duration::from_secs(5));
.connection_timeout(Some(Duration::from_secs(20)))
.send_timeout(Some(Duration::from_secs(10)))
.timeout(Duration::from_secs(20));
// Setup nostr client
let client = ClientBuilder::default()
@@ -277,7 +276,6 @@ fn main() {
// Create global state
app.manage(Nostr {
client,
contact_list: Mutex::new(vec![]),
settings: Mutex::new(Settings::default()),
circles: Mutex::new(HashMap::new()),
});
@@ -308,7 +306,11 @@ fn main() {
}
}
None => {
let contact_list = state.contact_list.lock().await;
let contact_list = client
.get_contact_list(Some(Duration::from_secs(5)))
.await
.unwrap();
if !contact_list.is_empty() {
let authors: Vec<PublicKey> =
contact_list.iter().map(|f| f.public_key).collect();
@@ -459,6 +461,39 @@ fn main() {
Ok(())
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::Focused(focused) = event {
if !focused {
let handle = window.app_handle().to_owned();
tauri::async_runtime::spawn(async move {
let state = handle.state::<Nostr>();
let client = &state.client;
if client.signer().await.is_ok() {
if let Ok(contact_list) =
client.get_contact_list(Some(Duration::from_secs(5))).await
{
let authors: Vec<PublicKey> =
contact_list.iter().map(|f| f.public_key).collect();
let newsfeed = Filter::new()
.authors(authors)
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(NEWSFEED_NEG_LIMIT);
if client
.reconcile(newsfeed, NegentropyOptions::default())
.await
.is_ok()
{
handle.emit("synchronized", ()).unwrap();
}
}
}
});
}
}
})
.plugin(prevent_default())
.plugin(tauri_plugin_decorum::init())
.plugin(tauri_plugin_store::Builder::default().build())