notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

cache.rs (7571B)


      1 use crate::{
      2     actionbar::TimelineOpenResult,
      3     error::Error,
      4     timeline::{Timeline, TimelineKind},
      5 };
      6 
      7 use notedeck::{filter, FilterState, NoteCache, NoteRef};
      8 
      9 use enostr::RelayPool;
     10 use nostrdb::{Filter, Ndb, Transaction};
     11 use std::collections::HashMap;
     12 use tracing::{debug, error, info, warn};
     13 
     14 #[derive(Default)]
     15 pub struct TimelineCache {
     16     timelines: HashMap<TimelineKind, Timeline>,
     17 }
     18 
     19 pub enum Vitality<'a, M> {
     20     Fresh(&'a mut M),
     21     Stale(&'a mut M),
     22 }
     23 
     24 impl<'a, M> Vitality<'a, M> {
     25     pub fn get_ptr(self) -> &'a mut M {
     26         match self {
     27             Self::Fresh(ptr) => ptr,
     28             Self::Stale(ptr) => ptr,
     29         }
     30     }
     31 
     32     pub fn is_stale(&self) -> bool {
     33         match self {
     34             Self::Fresh(_ptr) => false,
     35             Self::Stale(_ptr) => true,
     36         }
     37     }
     38 }
     39 
     40 impl<'a> IntoIterator for &'a mut TimelineCache {
     41     type Item = (&'a TimelineKind, &'a mut Timeline);
     42     type IntoIter = std::collections::hash_map::IterMut<'a, TimelineKind, Timeline>;
     43 
     44     fn into_iter(self) -> Self::IntoIter {
     45         self.timelines.iter_mut()
     46     }
     47 }
     48 
     49 impl TimelineCache {
     50     /// Pop a timeline from the timeline cache. This only removes the timeline
     51     /// if it has reached 0 subscribers, meaning it was the last one to be
     52     /// removed
     53     pub fn pop(
     54         &mut self,
     55         id: &TimelineKind,
     56         ndb: &mut Ndb,
     57         pool: &mut RelayPool,
     58     ) -> Result<(), Error> {
     59         let timeline = if let Some(timeline) = self.timelines.get_mut(id) {
     60             timeline
     61         } else {
     62             return Err(Error::TimelineNotFound);
     63         };
     64 
     65         timeline.subscription.unsubscribe_or_decrement(ndb, pool);
     66 
     67         if timeline.subscription.no_sub() {
     68             debug!(
     69                 "popped last timeline {:?}, removing from timeline cache",
     70                 id
     71             );
     72             self.timelines.remove(id);
     73         }
     74 
     75         Ok(())
     76     }
     77 
     78     fn get_expected_mut(&mut self, key: &TimelineKind) -> &mut Timeline {
     79         self.timelines
     80             .get_mut(key)
     81             .expect("expected notes in timline cache")
     82     }
     83 
     84     /// Insert a new timeline into the cache, based on the TimelineKind
     85     #[allow(clippy::too_many_arguments)]
     86     fn insert_new(
     87         &mut self,
     88         id: TimelineKind,
     89         txn: &Transaction,
     90         ndb: &Ndb,
     91         notes: &[NoteRef],
     92         note_cache: &mut NoteCache,
     93     ) {
     94         let mut timeline = if let Some(timeline) = id.clone().into_timeline(txn, ndb) {
     95             timeline
     96         } else {
     97             error!("Error creating timeline from {:?}", &id);
     98             return;
     99         };
    100 
    101         // insert initial notes into timeline
    102         timeline.insert_new(txn, ndb, note_cache, notes);
    103         self.timelines.insert(id, timeline);
    104     }
    105 
    106     pub fn insert(&mut self, id: TimelineKind, timeline: Timeline) {
    107         if let Some(cur_timeline) = self.timelines.get_mut(&id) {
    108             cur_timeline.subscription.increment();
    109             return;
    110         };
    111 
    112         self.timelines.insert(id, timeline);
    113     }
    114 
    115     /// Get and/or update the notes associated with this timeline
    116     pub fn notes<'a>(
    117         &'a mut self,
    118         ndb: &Ndb,
    119         note_cache: &mut NoteCache,
    120         txn: &Transaction,
    121         id: &TimelineKind,
    122     ) -> Vitality<'a, Timeline> {
    123         // we can't use the naive hashmap entry API here because lookups
    124         // require a copy, wait until we have a raw entry api. We could
    125         // also use hashbrown?
    126 
    127         if self.timelines.contains_key(id) {
    128             return Vitality::Stale(self.get_expected_mut(id));
    129         }
    130 
    131         let notes = if let FilterState::Ready(filters) = id.filters(txn, ndb) {
    132             if let Ok(results) = ndb.query(txn, filters.local(), 1000) {
    133                 results
    134                     .into_iter()
    135                     .map(NoteRef::from_query_result)
    136                     .collect()
    137             } else {
    138                 debug!("got no results from TimelineCache lookup for {:?}", id);
    139                 vec![]
    140             }
    141         } else {
    142             // filter is not ready yet
    143             vec![]
    144         };
    145 
    146         if notes.is_empty() {
    147             warn!("NotesHolder query returned 0 notes? ")
    148         } else {
    149             info!("found NotesHolder with {} notes", notes.len());
    150         }
    151 
    152         self.insert_new(id.to_owned(), txn, ndb, &notes, note_cache);
    153 
    154         Vitality::Fresh(self.get_expected_mut(id))
    155     }
    156 
    157     /// Open a timeline, this is another way of saying insert a timeline
    158     /// into the timeline cache. If there exists a timeline already, we
    159     /// bump its subscription reference count. If it's new we start a new
    160     /// subscription
    161     pub fn open(
    162         &mut self,
    163         ndb: &Ndb,
    164         note_cache: &mut NoteCache,
    165         txn: &Transaction,
    166         pool: &mut RelayPool,
    167         id: &TimelineKind,
    168     ) -> Option<TimelineOpenResult> {
    169         let (open_result, timeline) = match self.notes(ndb, note_cache, txn, id) {
    170             Vitality::Stale(timeline) => {
    171                 // The timeline cache is stale, let's update it
    172                 let notes = find_new_notes(
    173                     timeline.all_or_any_notes(),
    174                     timeline.subscription.get_filter()?.local(),
    175                     txn,
    176                     ndb,
    177                 );
    178                 let open_result = if notes.is_empty() {
    179                     None
    180                 } else {
    181                     let new_notes = notes.iter().map(|n| n.key).collect();
    182                     Some(TimelineOpenResult::new_notes(new_notes, id.clone()))
    183                 };
    184 
    185                 // we can't insert and update the VirtualList now, because we
    186                 // are already borrowing it mutably. Let's pass it as a
    187                 // result instead
    188                 //
    189                 // holder.get_view().insert(&notes); <-- no
    190                 (open_result, timeline)
    191             }
    192 
    193             Vitality::Fresh(timeline) => (None, timeline),
    194         };
    195 
    196         if let Some(filter) = timeline.filter.get_any_ready() {
    197             debug!("got open with *new* subscription for {:?}", &timeline.kind);
    198             timeline.subscription.try_add_local(ndb, filter);
    199             timeline.subscription.try_add_remote(pool, filter);
    200         } else {
    201             // This should never happen reasoning, self.notes would have
    202             // failed above if the filter wasn't ready
    203             error!(
    204                 "open: filter not ready, so could not setup subscription. this should never happen"
    205             );
    206         };
    207 
    208         timeline.subscription.increment();
    209 
    210         open_result
    211     }
    212 
    213     pub fn get(&self, id: &TimelineKind) -> Option<&Timeline> {
    214         self.timelines.get(id)
    215     }
    216 
    217     pub fn get_mut(&mut self, id: &TimelineKind) -> Option<&mut Timeline> {
    218         self.timelines.get_mut(id)
    219     }
    220 
    221     pub fn num_timelines(&self) -> usize {
    222         self.timelines.len()
    223     }
    224 }
    225 
    226 /// Look for new thread notes since our last fetch
    227 fn find_new_notes(
    228     notes: &[NoteRef],
    229     filters: &[Filter],
    230     txn: &Transaction,
    231     ndb: &Ndb,
    232 ) -> Vec<NoteRef> {
    233     if notes.is_empty() {
    234         return vec![];
    235     }
    236 
    237     let last_note = notes[0];
    238     let filters = filter::make_filters_since(filters, last_note.created_at + 1);
    239 
    240     if let Ok(results) = ndb.query(txn, &filters, 1000) {
    241         debug!("got {} results from NotesHolder update", results.len());
    242         results
    243             .into_iter()
    244             .map(NoteRef::from_query_result)
    245             .collect()
    246     } else {
    247         debug!("got no results from NotesHolder update",);
    248         vec![]
    249     }
    250 }