mirror of
https://github.com/nostrlabs-io/notepush.git
synced 2025-06-16 11:48:51 +00:00
Add Nostr event cache
This commit adds a simple in-memory Nostr Event cache, to reduce the amount of bandwidth used as well as to improve performance. Testing ------- Setup: - Two iPhone simulators running Damus and on different accounts - Damus version: 774da239b92ed630fbf91fce42d9e233661c0d7f - Notepush: This commit - Push notifications turned on, setup to connect to localhost, and configured to receive DM notifications - Run Notepush with `RUST_LOG=DEBUG` env variable for debug logging Steps: 1. Send a DM from one account to another. - Push notification should arrive with a few seconds delay - Push notification logs should mention that the event was cached 2. Send a DM again. - Push notification should arrive immediately 3. Wait for more than a minute 4. Send a DM again. - Push notification should take a few seconds again - Push notification logs should mention that the cache item expired and was deleted Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Closes: https://github.com/damus-io/notepush/issues/3
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
pub mod nostr_network_helper;
|
pub mod nostr_network_helper;
|
||||||
mod nostr_event_extensions;
|
mod nostr_event_extensions;
|
||||||
|
mod nostr_event_cache;
|
||||||
pub mod notification_manager;
|
pub mod notification_manager;
|
||||||
|
|
||||||
pub use nostr_network_helper::NostrNetworkHelper;
|
pub use nostr_network_helper::NostrNetworkHelper;
|
||||||
|
144
src/notification_manager/nostr_event_cache.rs
Normal file
144
src/notification_manager/nostr_event_cache.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
use crate::utils::time_delta::TimeDelta;
|
||||||
|
use tokio::time::Duration;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use log;
|
||||||
|
|
||||||
|
use super::nostr_event_extensions::MaybeConvertibleToMuteList;
|
||||||
|
|
||||||
|
struct CacheEntry {
|
||||||
|
event: Option<Event>, // `None` means the event does not exist as far as we know (It does NOT mean expired)
|
||||||
|
added_at: nostr::Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheEntry {
|
||||||
|
fn is_expired(&self, max_age: Duration) -> bool {
|
||||||
|
let time_delta = TimeDelta::subtracting(nostr::Timestamp::now(), self.added_at);
|
||||||
|
time_delta.negative || (time_delta.delta_abs_seconds > max_age.as_secs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Cache {
|
||||||
|
entries: HashMap<EventId, Arc<CacheEntry>>,
|
||||||
|
mute_lists: HashMap<PublicKey, Arc<CacheEntry>>,
|
||||||
|
contact_lists: HashMap<PublicKey, Arc<CacheEntry>>,
|
||||||
|
max_age: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cache {
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
pub fn new(max_age: Duration) -> Self {
|
||||||
|
Cache {
|
||||||
|
entries: HashMap::new(),
|
||||||
|
mute_lists: HashMap::new(),
|
||||||
|
contact_lists: HashMap::new(),
|
||||||
|
max_age,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Adding items to the cache
|
||||||
|
|
||||||
|
pub fn add_optional_mute_list_with_author(&mut self, author: &PublicKey, mute_list: Option<Event>) {
|
||||||
|
if let Some(mute_list) = mute_list {
|
||||||
|
self.add_event(mute_list);
|
||||||
|
} else {
|
||||||
|
self.mute_lists.insert(
|
||||||
|
author.clone(),
|
||||||
|
Arc::new(CacheEntry {
|
||||||
|
event: None,
|
||||||
|
added_at: nostr::Timestamp::now(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_optional_contact_list_with_author(&mut self, author: &PublicKey, contact_list: Option<Event>) {
|
||||||
|
if let Some(contact_list) = contact_list {
|
||||||
|
self.add_event(contact_list);
|
||||||
|
} else {
|
||||||
|
self.contact_lists.insert(
|
||||||
|
author.clone(),
|
||||||
|
Arc::new(CacheEntry {
|
||||||
|
event: None,
|
||||||
|
added_at: nostr::Timestamp::now(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_event(&mut self, event: Event) {
|
||||||
|
let entry = Arc::new(CacheEntry {
|
||||||
|
event: Some(event.clone()),
|
||||||
|
added_at: nostr::Timestamp::now(),
|
||||||
|
});
|
||||||
|
self.entries.insert(event.id.clone(), entry.clone());
|
||||||
|
|
||||||
|
match event.kind {
|
||||||
|
Kind::MuteList => {
|
||||||
|
self.mute_lists.insert(event.pubkey.clone(), entry.clone());
|
||||||
|
log::debug!("Added mute list to the cache. Event ID: {}", event.id.to_hex());
|
||||||
|
}
|
||||||
|
Kind::ContactList => {
|
||||||
|
self.contact_lists
|
||||||
|
.insert(event.pubkey.clone(), entry.clone());
|
||||||
|
log::debug!("Added contact list to the cache. Event ID: {}", event.id.to_hex());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!("Added event to the cache. Event ID: {}", event.id.to_hex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Fetching items from the cache
|
||||||
|
|
||||||
|
pub fn get_mute_list(&mut self, pubkey: &PublicKey) -> Result<Option<MuteList>, CacheError> {
|
||||||
|
if let Some(entry) = self.mute_lists.get(pubkey) {
|
||||||
|
let entry = entry.clone(); // Clone the Arc to avoid borrowing issues
|
||||||
|
if !entry.is_expired(self.max_age) {
|
||||||
|
if let Some(event) = entry.event.clone() {
|
||||||
|
return Ok(event.to_mute_list());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!("Mute list for pubkey {} is expired, removing it from the cache", pubkey.to_hex());
|
||||||
|
self.mute_lists.remove(pubkey);
|
||||||
|
self.remove_event_from_all_maps(&entry.event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(CacheError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_contact_list(&mut self, pubkey: &PublicKey) -> Result<Option<Event>, CacheError> {
|
||||||
|
if let Some(entry) = self.contact_lists.get(pubkey) {
|
||||||
|
let entry = entry.clone(); // Clone the Arc to avoid borrowing issues
|
||||||
|
if !entry.is_expired(self.max_age) {
|
||||||
|
return Ok(entry.event.clone());
|
||||||
|
} else {
|
||||||
|
log::debug!("Contact list for pubkey {} is expired, removing it from the cache", pubkey.to_hex());
|
||||||
|
self.contact_lists.remove(pubkey);
|
||||||
|
self.remove_event_from_all_maps(&entry.event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(CacheError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Removing items from the cache
|
||||||
|
|
||||||
|
fn remove_event_from_all_maps(&mut self, event: &Option<Event>) {
|
||||||
|
if let Some(event) = event {
|
||||||
|
let event_id = event.id.clone();
|
||||||
|
let pubkey = event.pubkey.clone();
|
||||||
|
self.entries.remove(&event_id);
|
||||||
|
self.mute_lists.remove(&pubkey);
|
||||||
|
self.contact_lists.remove(&pubkey);
|
||||||
|
}
|
||||||
|
// We can't remove an event from all maps if the event does not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error type
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CacheError {
|
||||||
|
NotFound,
|
||||||
|
}
|
@ -1,12 +1,15 @@
|
|||||||
use super::nostr_event_extensions::MaybeConvertibleToMuteList;
|
use super::nostr_event_extensions::MaybeConvertibleToMuteList;
|
||||||
use super::ExtendedEvent;
|
use super::ExtendedEvent;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use super::nostr_event_cache::Cache;
|
||||||
use tokio::time::{timeout, Duration};
|
use tokio::time::{timeout, Duration};
|
||||||
|
|
||||||
const NOTE_FETCH_TIMEOUT: Duration = Duration::from_secs(5);
|
const NOTE_FETCH_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
const CACHE_MAX_AGE: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
pub struct NostrNetworkHelper {
|
pub struct NostrNetworkHelper {
|
||||||
client: Client,
|
client: Client,
|
||||||
|
cache: Cache,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NostrNetworkHelper {
|
impl NostrNetworkHelper {
|
||||||
@ -16,13 +19,14 @@ impl NostrNetworkHelper {
|
|||||||
let client = Client::new(&Keys::generate());
|
let client = Client::new(&Keys::generate());
|
||||||
client.add_relay(relay_url.clone()).await?;
|
client.add_relay(relay_url.clone()).await?;
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
Ok(NostrNetworkHelper { client })
|
|
||||||
|
Ok(NostrNetworkHelper { client, cache: Cache::new(CACHE_MAX_AGE) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Answering questions about a user
|
// MARK: - Answering questions about a user
|
||||||
|
|
||||||
pub async fn should_mute_notification_for_pubkey(
|
pub async fn should_mute_notification_for_pubkey(
|
||||||
&self,
|
&mut self,
|
||||||
event: &Event,
|
event: &Event,
|
||||||
pubkey: &PublicKey,
|
pubkey: &PublicKey,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@ -67,7 +71,7 @@ impl NostrNetworkHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn does_pubkey_follow_pubkey(
|
pub async fn does_pubkey_follow_pubkey(
|
||||||
&self,
|
&mut self,
|
||||||
source_pubkey: &PublicKey,
|
source_pubkey: &PublicKey,
|
||||||
target_pubkey: &PublicKey,
|
target_pubkey: &PublicKey,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@ -82,16 +86,32 @@ impl NostrNetworkHelper {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetching specific event types
|
// MARK: - Getting specific event types with caching
|
||||||
|
|
||||||
pub async fn get_public_mute_list(&self, pubkey: &PublicKey) -> Option<MuteList> {
|
pub async fn get_public_mute_list(&mut self, pubkey: &PublicKey) -> Option<MuteList> {
|
||||||
self.fetch_single_event(pubkey, Kind::MuteList)
|
match self.cache.get_mute_list(pubkey) {
|
||||||
.await?
|
Ok(optional_mute_list) => optional_mute_list,
|
||||||
.to_mute_list()
|
Err(_) => {
|
||||||
|
// We don't have an answer from the cache, so we need to fetch it
|
||||||
|
let mute_list_event = self.fetch_single_event(pubkey, Kind::MuteList)
|
||||||
|
.await;
|
||||||
|
self.cache.add_optional_mute_list_with_author(pubkey, mute_list_event.clone());
|
||||||
|
mute_list_event?.to_mute_list()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_contact_list(&self, pubkey: &PublicKey) -> Option<Event> {
|
pub async fn get_contact_list(&mut self, pubkey: &PublicKey) -> Option<Event> {
|
||||||
self.fetch_single_event(pubkey, Kind::ContactList).await
|
match self.cache.get_contact_list(pubkey) {
|
||||||
|
Ok(optional_contact_list) => optional_contact_list,
|
||||||
|
Err(_) => {
|
||||||
|
// We don't have an answer from the cache, so we need to fetch it
|
||||||
|
let contact_list_event = self.fetch_single_event(pubkey, Kind::ContactList)
|
||||||
|
.await;
|
||||||
|
self.cache.add_optional_contact_list_with_author(pubkey, contact_list_event.clone());
|
||||||
|
contact_list_event
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Lower level fetching functions
|
// MARK: - Lower level fetching functions
|
||||||
|
@ -27,7 +27,6 @@ pub struct NotificationManager {
|
|||||||
db: Mutex<r2d2::Pool<SqliteConnectionManager>>,
|
db: Mutex<r2d2::Pool<SqliteConnectionManager>>,
|
||||||
apns_topic: String,
|
apns_topic: String,
|
||||||
apns_client: Mutex<Client>,
|
apns_client: Mutex<Client>,
|
||||||
|
|
||||||
nostr_network_helper: Mutex<NostrNetworkHelper>,
|
nostr_network_helper: Mutex<NostrNetworkHelper>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +220,7 @@ impl NotificationManager {
|
|||||||
let mut pubkeys_to_notify = HashSet::new();
|
let mut pubkeys_to_notify = HashSet::new();
|
||||||
for pubkey in relevant_pubkeys_yet_to_receive {
|
for pubkey in relevant_pubkeys_yet_to_receive {
|
||||||
let should_mute: bool = {
|
let should_mute: bool = {
|
||||||
let mute_manager_mutex_guard = self.nostr_network_helper.lock().await;
|
let mut mute_manager_mutex_guard = self.nostr_network_helper.lock().await;
|
||||||
mute_manager_mutex_guard
|
mute_manager_mutex_guard
|
||||||
.should_mute_notification_for_pubkey(event, &pubkey)
|
.should_mute_notification_for_pubkey(event, &pubkey)
|
||||||
.await
|
.await
|
||||||
@ -286,7 +285,7 @@ impl NotificationManager {
|
|||||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
let notification_preferences = self.get_user_notification_settings(pubkey, device_token).await?;
|
let notification_preferences = self.get_user_notification_settings(pubkey, device_token).await?;
|
||||||
if notification_preferences.only_notifications_from_following_enabled {
|
if notification_preferences.only_notifications_from_following_enabled {
|
||||||
let nostr_network_helper_mutex_guard = self.nostr_network_helper.lock().await;
|
let mut nostr_network_helper_mutex_guard = self.nostr_network_helper.lock().await;
|
||||||
if !nostr_network_helper_mutex_guard.does_pubkey_follow_pubkey(pubkey, &event.author()).await {
|
if !nostr_network_helper_mutex_guard.does_pubkey_follow_pubkey(pubkey, &event.author()).await {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user