notedeck

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

cache.rs (7156B)


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